Go パッケージの作成とインポート

Go 言語Beginner
オンラインで実践に進む

はじめに

前のセクションでは、次のコード行を含む基本的な Go プログラムを作成しました。

package main
import "fmt"

これらの 2 行のコードをどのように理解すればよいでしょうか。また、packageとimport文を効果的に使うにはどうすればよいでしょうか。

この実験では、Go でパッケージを作成してインポートする方法を学びます。これにより、コードを再利用可能なモジュールに整理して、Go プロジェクトをより保守しやすく拡張可能にすることができます。

ポイント

  • パッケージの定義と宣言
  • エクスポートされた(公開)識別子とエクスポートされていない(非公開)識別子の理解
  • パッケージのインポートのさまざまな形式:単一、グループ化、ドット、エイリアス、および匿名インポート

パッケージの宣言と定義

このステップでは、再利用可能なパッケージを宣言し、エクスポートされる識別子とパッケージ内部だけで使える識別子を区別します。

Go におけるパッケージは、Python のモジュールや C のライブラリに似ています。コードを整理して再利用するために使用されるソースコードファイルのコレクションです。すべての Go ファイルは、ファイルの先頭でパッケージを宣言する必要があります。

注: Go の実行可能ファイルは、main 関数を含む main パッケージからビルドされます。再利用可能なパッケージには、このエントリポイントは不要です。一つのモジュールに複数のコマンド用ディレクトリを含め、それぞれが独自の main パッケージを持つこともできます。

ポイント

エクスポートされた(公開)識別子:大文字で始まる識別子(変数、関数、型など)は、他のパッケージからアクセスできます。これらは、パッケージの公開インターフェイスと考えられます。

エクスポートされていない(非公開)識別子:小文字で始まる識別子は、同じパッケージ内でのみアクセスできます。これらは、パッケージの内部実装の詳細と考えられます。

パッケージの凝集性:同じフォルダ内のすべてのファイルは、同じパッケージに属する必要があります。これにより、関連するコードがまとまったままになります。

パッケージの命名規則:パッケージ名は、小文字で、短く、説明的で、アンダースコアや大文字を避ける必要があります。

独自のカスタムパッケージを作成しましょう。

propagandistという名前のフォルダを作成し、その中にpropagandist.goというファイルを作成します。

mkdir ~/project/propagandist
touch ~/project/propagandist/propagandist.go

propagandist.goに次のコードを記述します。

package propagandist

var Shout = "I Love LabEx" // Public variable
var secret = "I love the dress" // Private variable

func Hit() string {
    return "Don't hit me, please!"
}
  • Shoutは公開で、大文字で始まるため、propagandistをインポートする他のパッケージからアクセスできます。
  • secretは非公開で、小文字で始まるため、propagandistパッケージ内でのみ使用できます。
  • Hitは公開関数で、他のパッケージからアクセスできます。

パッケージ用の Go モジュールを初期化します。

cd ~/project/propagandist
go mod init propagandist

このコマンドは、propagandistディレクトリに新しい Go モジュールを初期化し、パッケージの依存関係を管理するのに役立ちます。

単項目インポート

このステップでは、個別の import 宣言を使って、ローカルパッケージをインポートして利用します。

propagandistパッケージを使用するには、新しい Go プログラムを作成しましょう。この手順では、Go コードで単一のパッケージをインポートして使用する方法を示します。

プロジェクトフォルダに新しい Go ファイルpacExercise.goを作成します。

touch ~/project/pacExercise.go

プログラム用の Go モジュールを初期化します。

cd ~/project
go mod init pacExercise

go.modファイルを更新して、ローカルパッケージの依存関係を追加します。ターミナルで次のコマンドを実行します。

echo "replace propagandist => ./propagandist" >> go.mod

重要: このコマンドは、go.modファイルにreplaceディレクティブを追加します。これは重要です。なぜなら、これにより Go に対して、propagandistパッケージをリモートリポジトリからダウンロードしようとせずに、ローカルディレクトリ./propagandistからソースを取得するように指示するからです。このコマンドをターミナルで実行すると、go.modファイルにreplace propagandist => ./propagandistの行が追加されます。直接この行をファイルに手動で書き込まないでください。

pacExercise.goに次のコードを書いて、propagandistパッケージをインポートして使用します。

package main

import "fmt"
import "propagandist"

func main() {
    fmt.Println(propagandist.Shout) // Access the public variable
}
  • このコードは、出力を表示するためのfmtパッケージとpropagandistパッケージをインポートします。
  • その後、propagandist.Shoutを使用してpropagandistパッケージから公開変数Shoutにアクセスします。

プログラムを実行します。

go mod tidy
go run pacExercise.go

go mod tidyコマンドは、新しい依存関係でgo.modファイルを更新することを確認します。go run pacExercise.goコマンドは、プログラムをコンパイルして実行します。

予想される出力:

I Love LabEx

グループ化されたインポート

このステップでは、プログラムの動作を変えずに、パッケージのインポートをグループ化します。

複数のパッケージをインポートする際には、読みやすさと整理のためにグループ化されたインポートを使用できます。これはスタイルに関する選択事項であり、コードの機能には影響しません。

pacExercise.goを変更して、グループ化されたインポートを使用します。

```go
package main

import (
    "fmt"
    "propagandist"
)

func main() {
    fmt.Println(propagandist.Shout)
}
```

上のコードスニペットでは、fmtとpropagandistの各パッケージが、丸括弧で囲まれた単一のimportブロック内にインポートされています。これにより、複数のパッケージのインポートを読みやすく管理しやすくなります。前のステップの個別の import 宣言とは異なり、ここでは両方のパッケージパスを一つの宣言にまとめています。プログラムの動作は同じです。

プログラムを実行して、依然として機能することを確認します。

```bash
go run pacExercise.go
```

プログラムはエラーなく実行され、以前と同じ結果が出力されるはずです。

ドットインポート

このステップでは、ドットインポートと、名前空間に関する利点や注意点を確認します。

ドットインポートを使用すると、関数や変数を呼び出す際にパッケージ名の接頭辞を省略できます。名前空間の競合を引き起こし、読みやすさを低下させる可能性があるため、明示的なパッケージ名を推奨することが多いです。ただし、それが何であるかを知っておくのは良いでしょう。

pacExercise.goを変更して、fmtに対してドットインポートを使用します。

```go
package main

import . "fmt"
import "propagandist"

func main() {
    Println(propagandist.Shout) // No `fmt.` prefix needed
}
```
  • ここで、import . "fmt"は、fmtパッケージの関数や変数をfmt.接頭辞なしで直接使用できることを意味します。
  • たとえば、fmt.Printlnの代わりにPrintlnを使用します。

プログラムを実行します。

```bash
go run pacExercise.go
```

予想される出力:

```bash
I Love LabEx
```

エイリアスインポート

このステップでは、インポートしたパッケージにローカルの別名を付けます。

2 つのパッケージが似た名前を持っている場合、明確さを保つためまたは競合を回避するために、インポートされたパッケージにエイリアスを付けることができます。これは、コードの読みやすさを向上させ、名前空間の衝突を管理するのに役立ちます。

pacExercise.goを変更して、fmtをioとしてエイリアス付けします。

```go
package main

import io "fmt"
import "propagandist"

func main() {
    io.Println(propagandist.Shout) // Use the alias `io` instead of `fmt`
}
```
  • import io "fmt"は、fmtパッケージに対してエイリアスioを作成します。
  • これで、fmt.Printlnの代わりにio.Printlnを使用します。

プログラムを実行します。

```bash
go run pacExercise.go
```

匿名インポート

このステップでは、初期化時の副作用だけを目的としてパッケージをインポートします。

匿名インポートは、副作用のためにパッケージをインポートするために使用されます。たとえば、そのinit()関数を実行するために、そのエクスポートされた識別子のいずれかを直接参照する必要がない場合です。これは、ドライバを登録するパッケージやその他の初期化タスクを実行するパッケージに役立ちます。

pacExercise.goを変更して、timeの匿名インポートを含めます。

```go
package main

import (
    "fmt"
    "propagandist"
    _ "time" // Anonymous import
)

func main() {
    fmt.Println(propagandist.Shout)
}
```
  • import _ "time"は匿名インポートです。アンダースコア_はブランク識別子として使用され、コンパイラに対して、副作用のためにパッケージをインポートしており、コード内でそれから直接何も参照しないことを伝えます。
  • このプログラムが実行されるとき、timeパッケージのinit()関数が実行されます。ここではtimeパッケージには特に目に見える副作用はありませんが、多くのパッケージではこれを使用してデータベースドライバや構成設定を登録しています。

プログラムを実行します。

```bash
go run pacExercise.go
```

予想される出力:

```bash
I Love LabEx
```

init() 関数を理解する

このステップでは、main() が実行される前の、パッケージの変数と init() 関数の処理を追います。

init() 関数は、パッケージの初期化で重要な役割を果たす Go の特別な関数です。パッケージがインポートされると、main 関数内のコードが実行される前に Go が自動的に実行します。このステップでは、init() 関数の詳細と、Go の初期化処理における動作を学びます。

init() 関数の重要なポイント:

定義と目的:

  • init() 関数には引数も戻り値もありません。形式は func init() {} です。
  • 初期状態の設定、ドライバーの登録、前提条件の検証など、パッケージの初期化処理に使います。

実行順序:

  • 同じパッケージが複数回インポートされても、Go はそのパッケージを一度だけ初期化します。
  • 初期化は次の順序で進みます。
  • 最初にパッケージレベルの変数が初期化されます。
  • 次に init() 関数が実行されます。
  • 最後に main() 関数が実行されます(main パッケージのみ)。

複数の init() 関数:

  • 一つの Go ファイルに複数の init() 関数を定義できます。
  • 同じパッケージ内の複数のファイルが、それぞれ init() 関数を持つこともできます。
  • これらの init() 関数はすべて、コンパイラーにファイルが渡される順序に従い、ソース内の宣言順に実行されます。Go ツールは、パッケージのファイルをファイル名の辞書順で渡します。依存関係を表すためにファイルの順序に頼ることは避けてください。

依存関係の連鎖:

  • パッケージが別のパッケージをインポートする場合、Go は依存先の init() 関数を先に実行します。
  • そのため、依存関係の最も深いパッケージから順に初期化されます。

init() 関数の動作を確認するため、具体例を作成しましょう。

まず、propagandist パッケージに init() 関数を追加します。propagandist.go を次のように更新してください。

package propagandist

import "fmt"

var Shout = "I Love LabEx" // Public variable
var secret = "I love the dress" // Private variable
var initialized bool

func init() {
    fmt.Println("Initializing propagandist package...")
    initialized = true
}

func Hit() string {
    return "Don't hit me, please!"
}

func IsInitialized() bool {
    return initialized
}

次に、複数の init() 関数の動作を確認するため、propagandist パッケージ内に別のファイルを作成します。

touch ~/project/propagandist/second.go

このファイルに次の内容を追加します。

package propagandist

import "fmt"

func init() {
    fmt.Println("Second init function in propagandist package...")
}

初期化順序を確認するため、新しい helper パッケージを作成します。

mkdir -p ~/project/helper
touch ~/project/helper/helper.go

このファイルに次の内容を追加します。

package helper

import "fmt"

var Message = "Helper package is ready"

func init() {
    fmt.Println("Initializing helper package...")
}

func GetMessage() string {
    return Message
}

helper パッケージのモジュールファイルを追加します。

cd ~/project/helper
go mod init helper

両方のパッケージを使って初期化順序を確認できるように、pacExercise.go を更新します。

package main

import (
    "fmt"
    "helper"
    "propagandist"
)

func init() {
    fmt.Println("Initializing main package...")
}

func main() {
    fmt.Println("Main function is running")
    fmt.Println(propagandist.Shout)
    fmt.Println(helper.Message)
    fmt.Printf("Propagandist initialized: %v\n", propagandist.IsInitialized())
}

メインプロジェクトの go.mod ファイルを更新して、ローカルの helper パッケージを追加します。

cd ~/project
echo "replace helper => ./helper" >> go.mod
go mod tidy

プログラムを実行し、初期化順序を観察します。

go run pacExercise.go

指定されたファイル名と Go ツールを使った場合の予想される出力:

Initializing helper package...
Initializing propagandist package...
Second init function in propagandist package...
Initializing main package...
Main function is running
I Love LabEx
Helper package is ready
Propagandist initialized: true

この出力は、Go の初期化処理の重要な点を示しています。

依存先のパッケージは、それをインポートするパッケージより先に初期化されます。

一つのパッケージ内の init() 関数はすべて、コンパイラーにソースファイルが渡される順序に従い、宣言順に実行されます。

すべてのパッケージの初期化が完了してから main() 関数が実行されます。

パッケージレベルの変数は、init() 関数が実行される前に初期化されます。

Go パッケージを設計する際は、この初期化順序を考慮することが重要です。特に、依存関係を管理する場合や、特定の順序で必要な準備処理を行う場合に関係します。

まとめ

この実験では、以下のことを学びました。

  1. Go 言語でカスタムパッケージを作成して定義し、再利用可能なコードをカプセル化する方法
  2. 公開(エクスポート)された識別子と非公開(エクスポートされていない)識別子の違いと、それがアクセシビリティに与える影響
  3. パッケージをインポートするさまざまな方法とそのそれぞれの使用例
    • 単項目インポート:1 回に 1 つのパッケージをインポートする
    • グループ化されたインポート:整理をよくするために 1 つのブロックで複数のパッケージをインポートする
    • ドットインポート:パッケージ名の接頭辞なしで直接その識別子を使用してパッケージをインポートする(注意して使用)
    • エイリアスインポート:読みやすさを向上させるか、または名前の競合を回避するためにインポートされたパッケージを改名する
    • 匿名インポート:初期化などの副作用のためだけにパッケージをインポートする
  4. パッケージ内の init() 関数の役割と、匿名インポートがその実行を引き起こす仕組み
  5. Go の初期化処理の詳細
    • パッケージレベルの変数が init() 関数より前に初期化されること
    • 依存関係のあるパッケージ間で保証される init() 関数の実行順序
    • 同じパッケージ内での複数の init() 関数の動作
    • 依存先パッケージから main 関数までの初期化全体の流れ

この実験を完了することで、パッケージを使って Go プロジェクトを効果的に構造化して管理できるようになりました。再利用可能なモジュールを作成し、識別子へのアクセスを制御し、コードを整理し、初期化処理を理解して、より保守しやすく拡張可能な Go アプリケーションを作成できます。