Goのinitを理解する
序章
Goでは、事前定義されたinit()
関数が、パッケージの他の部分の前に実行されるコードの一部を開始します。 このコードは、パッケージがインポートされるとすぐに実行され、特定の構成やリソースのセットがある場合など、アプリケーションを特定の状態で初期化する必要がある場合に使用できます。開始する必要があります。 また、副作用をインポートするときにも使用されます。これは、特定のパッケージをインポートしてプログラムの状態を設定するために使用される手法です。 これは、プログラムがタスクの正しいコードを検討していることを確認するために、あるパッケージを別のパッケージとregister
するためによく使用されます。
init()
は便利なツールですが、見つけにくいinit()
インスタンスはコードの実行順序に大きく影響するため、コードが読みにくくなる場合があります。 このため、Goを初めて使用する開発者は、この関数の側面を理解して、コードを作成するときにinit()
を読みやすく使用できるようにすることが重要です。
このチュートリアルでは、init()
が特定のパッケージ変数のセットアップと初期化、1回限りの計算、および別のパッケージで使用するパッケージの登録にどのように使用されるかを学習します。
前提条件
この記事のいくつかの例では、次のものが必要になります。
- Goのインストール方法とローカルプログラミング環境のセットアップに従ってセットアップされたGoワークスペース。 このチュートリアルでは、次のファイル構造を使用します。
. ├── bin │ └── src └── github.com └── gopherguides
init()
を宣言する
init()
関数を宣言するときはいつでも、Goはそのパッケージ内の他の何よりも先にそれをロードして実行します。 これを実証するために、このセクションでは、init()
関数を定義する方法を説明し、パッケージの実行方法への影響を示します。
まず、init()
関数を使用しないコードの例として次の例を取り上げます。
main.go
package main import "fmt" var weekday string func main() { fmt.Printf("Today is %s", weekday) }
このプログラムでは、weekday
というグローバル変数を宣言しました。 デフォルトでは、weekday
の値は空の文字列です。
このコードを実行してみましょう:
go run main.go
weekday
の値が空白であるため、プログラムを実行すると、次の出力が得られます。
OutputToday is
weekday
の値を現在の日に初期化するinit()
関数を導入することにより、空白の変数を埋めることができます。 次の強調表示された行をmain.go
に追加します。
main.go
package main import ( "fmt" "time" ) var weekday string func init() { weekday = time.Now().Weekday().String() } func main() { fmt.Printf("Today is %s", weekday) }
このコードでは、time
パッケージをインポートして使用し、現在の曜日(Now().Weekday().String()
)を取得してから、init()
を使用してweekday
を初期化しました。その値。
プログラムを実行すると、現在の平日が出力されます。
OutputToday is Monday
これはinit()
がどのように機能するかを示していますが、init()
のより一般的な使用例は、パッケージをインポートするときに使用することです。 これは、パッケージを使用する前に、パッケージで特定のセットアップタスクを実行する必要がある場合に役立ちます。 これを実証するために、パッケージが意図したとおりに機能するために特定の初期化を必要とするプログラムを作成しましょう。
インポート時にパッケージを初期化する
まず、スライスからランダムなクリーチャーを選択して出力するコードを記述します。 ただし、最初のプログラムではinit()
を使用しません。 これにより、私たちが抱えている問題と、init()
がどのように問題を解決するかがわかりやすくなります。
src/github.com/gopherguides/
ディレクトリ内から、次のコマンドを使用してcreature
というフォルダを作成します。
mkdir creature
creature
フォルダー内に、creature.go
というファイルを作成します。
nano creature/creature.go
このファイルに、次の内容を追加します。
クリーチャー.go
package creature import ( "math/rand" ) var creatures = []string{"shark", "jellyfish", "squid", "octopus", "dolphin"} func Random() string { i := rand.Intn(len(creatures)) return creatures[i] }
このファイルは、値として初期化された海の生き物のセットを持つcreatures
と呼ばれる変数を定義します。 また、 exported Random
関数があり、creatures
変数からランダムな値を返します。
このファイルを保存して終了します。
次に、main()
関数を記述し、creature
パッケージを呼び出すために使用するcmd
パッケージを作成しましょう。
creature
フォルダーを作成したのと同じファイルレベルで、次のコマンドを使用してcmd
フォルダーを作成します。
mkdir cmd
cmd
フォルダー内に、main.go
というファイルを作成します。
nano cmd/main.go
次の内容をファイルに追加します。
cmd / main.go
package main import ( "fmt" "github.com/gopherguides/creature" ) func main() { fmt.Println(creature.Random()) fmt.Println(creature.Random()) fmt.Println(creature.Random()) fmt.Println(creature.Random()) }
ここでは、creature
パッケージをインポートし、main()
関数で、creature.Random()
関数を使用してランダムなクリーチャーを取得し、4回印刷しました。
main.go
を保存して終了します。
これで、プログラム全体が作成されました。 ただし、このプログラムを実行する前に、コードを正しく機能させるために、いくつかの構成ファイルも作成する必要があります。 Goは、 Go Modules を使用して、リソースをインポートするためのパッケージの依存関係を構成します。 これらのモジュールは、パッケージディレクトリに配置された構成ファイルであり、コンパイラにパッケージのインポート元を指示します。 モジュールについて学ぶことはこの記事の範囲を超えていますが、この例をローカルで機能させるために、ほんの数行の構成を書くことができます。
cmd
ディレクトリに、go.mod
という名前のファイルを作成します。
nano cmd/go.mod
ファイルが開いたら、次の内容に配置します。
cmd / go.mod
module github.com/gopherguides/cmd replace github.com/gopherguides/creature => ../creature
このファイルの最初の行は、作成したcmd
パッケージが実際にはgithub.com/gopherguides/cmd
であることをコンパイラーに通知します。 2行目は、github.com/gopherguides/creature
がディスク上の../creature
ディレクトリのローカルにあることをコンパイラに通知します。
ファイルを保存して閉じます。 次に、creature
ディレクトリにgo.mod
ファイルを作成します。
nano creature/go.mod
次のコード行をファイルに追加します。
クリーチャー/go.mod
module github.com/gopherguides/creature
これは、作成したcreature
パッケージが実際にはgithub.com/gopherguides/creature
パッケージであることをコンパイラーに通知します。 これがないと、cmd
パッケージはこのパッケージのインポート元を認識できません。
ファイルを保存して終了します。
これで、次のディレクトリ構造とファイルレイアウトが作成されます。
├── cmd │ ├── go.mod │ └── main.go └── creature ├── go.mod └── creature.go
すべての構成が完了したので、次のコマンドを使用してmain
プログラムを実行できます。
go run cmd/main.go
これにより、次のようになります。
Outputjellyfish squid squid dolphin
このプログラムを実行すると、4つの値を受け取り、それらを印刷しました。 プログラムを複数回実行すると、常にが、期待どおりのランダムな結果ではなく、同じ出力を取得することに気付くでしょう。 これは、rand
パッケージが疑似乱数を作成し、単一の初期状態に対して同じ出力を一貫して生成するためです。 よりランダムな数を実現するには、パッケージをシードするか、プログラムを実行するたびに初期状態が異なるようにソースを変更するように設定します。 Goでは、現在の時刻を使用してrand
パッケージをシードするのが一般的です。
creature
パッケージでランダム機能を処理する必要があるため、次のファイルを開きます。
nano creature/creature.go
次の強調表示された行をcreature.go
ファイルに追加します。
クリーチャー/creature.go
package creature import ( "math/rand" "time" ) var creatures = []string{"shark", "jellyfish", "squid", "octopus", "dolphin"} func Random() string { rand.Seed(time.Now().UnixNano()) i := rand.Intn(len(creatures)) return creatures[i] }
このコードでは、time
パッケージをインポートし、Seed()
を使用して現在の時刻をシードしました。 ファイルを保存して終了します。
これで、プログラムを実行すると、ランダムな結果が得られます。
go run cmd/main.go
Outputjellyfish octopus shark jellyfish
プログラムを何度も実行し続けると、ランダムな結果が得られ続けます。 ただし、これはまだコードの理想的な実装ではありません。creature.Random()
が呼び出されるたびに、rand.Seed(time.Now().UnixNano())
を再度呼び出すことによってrand
パッケージも再シードするためです。 再シードすると、内部クロックが変更されていない場合に同じ初期値でシードされる可能性が高くなり、ランダムパターンが繰り返される可能性があります。または、プログラムがクロックの変更を待機することにより、CPU処理時間が長くなります。
これを修正するには、init()
関数を使用できます。 creature.go
ファイルを更新しましょう。
nano creature/creature.go
次のコード行を追加します。
クリーチャー/creature.go
package creature import ( "math/rand" "time" ) var creatures = []string{"shark", "jellyfish", "squid", "octopus", "dolphin"} func init() { rand.Seed(time.Now().UnixNano()) } func Random() string { i := rand.Intn(len(creatures)) return creatures[i] }
init()
関数を追加すると、creature
パッケージをインポートするときに、init()
関数を1回実行して、乱数生成用の単一シードを提供する必要があることをコンパイラーに通知します。 これにより、必要以上にコードを実行しないようになります。 プログラムを実行すると、ランダムな結果が得られます。
go run cmd/main.go
Outputdolphin squid dolphin octopus
このセクションでは、init()
を使用すると、パッケージを使用する前に適切な計算または初期化を確実に実行できることを確認しました。 次に、パッケージで複数のinit()
ステートメントを使用する方法を説明します。
init()
の複数のインスタンス
一度しか宣言できないmain()
関数とは異なり、init()
関数はパッケージ全体で複数回宣言できます。 ただし、init()
が複数あると、どれが他よりも優先されるかを判断するのが難しくなる可能性があります。 このセクションでは、複数のinit()
ステートメントの制御を維持する方法を示します。
ほとんどの場合、init()
関数は、遭遇した順序で実行されます。 例として次のコードを取り上げましょう。
main.go
package main import "fmt" func init() { fmt.Println("First init") } func init() { fmt.Println("Second init") } func init() { fmt.Println("Third init") } func init() { fmt.Println("Fourth init") } func main() {}
次のコマンドでプログラムを実行すると、次のようになります。
go run main.go
次の出力が表示されます。
OutputFirst init Second init Third init Fourth init
各init()
は、コンパイラーが検出した順序で実行されることに注意してください。 ただし、init()
関数が呼び出される順序を決定するのは必ずしも簡単ではない場合があります。
それぞれが独自のinit()
関数が宣言された複数のファイルがある、より複雑なパッケージ構造を見てみましょう。 これを説明するために、message
という変数を共有するプログラムを作成して出力します。
creature
およびcmd
ディレクトリとその内容を前のセクションから削除し、次のディレクトリとファイル構造に置き換えます。
├── cmd │ ├── a.go │ ├── b.go │ └── main.go └── message └── message.go
次に、各ファイルの内容を追加しましょう。 a.go
に、次の行を追加します。
cmd / a.go
package main import ( "fmt" "github.com/gopherguides/message" ) func init() { fmt.Println("a ->", message.Message) }
このファイルには、message
パッケージからmessage.Message
の値を出力する単一のinit()
関数が含まれています。
次に、b.go
に次の内容を追加します。
cmd / b.go
package main import ( "fmt" "github.com/gopherguides/message" ) func init() { message.Message = "Hello" fmt.Println("b ->", message.Message) }
b.go
には、message.Message
の値をHello
に設定して出力する単一のinit()
関数があります。
次に、次のようにmain.go
を作成します。
cmd / main.go
package main func main() {}
このファイルは何もしませんが、プログラムを実行するためのエントリポイントを提供します。
最後に、次のようにmessage.go
ファイルを作成します。
message / message.go
package message var Message string
message
パッケージは、エクスポートされたMessage
変数を宣言します。
プログラムを実行するには、cmd
ディレクトリから次のコマンドを実行します。
go run *.go
cmd
フォルダーにはmain
パッケージを構成する複数のGoファイルがあるため、cmd
内のすべての.go
ファイルをコンパイラーに通知する必要があります。 ]フォルダをコンパイルする必要があります。 *.go
を使用すると、.go
で終わるcmd
フォルダー内のすべてのファイルをロードするようにコンパイラーに指示されます。 go run main.go
のコマンドを発行した場合、a.go
およびb.go
ファイルのコードが表示されないため、プログラムはコンパイルに失敗します。
これにより、次の出力が得られます。
Outputa -> b -> Hello
Package Initialization のGo言語仕様に従って、パッケージ内で複数のファイルが検出されると、それらはアルファベット順に処理されます。 このため、a.go
からmessage.Message
を初めて印刷したときは、値が空白でした。 b.go
のinit()
関数が実行されるまで、値は初期化されませんでした。
a.go
のファイル名をc.go
に変更すると、異なる結果が得られます。
Outputb -> Hello a -> Hello
これで、コンパイラは最初にb.go
に遭遇するため、c.go
のinit()
関数の場合、message.Message
の値はHello
ですでに初期化されています。 ]に遭遇しました。
この動作により、コードに問題が発生する可能性があります。 ソフトウェア開発ではファイル名を変更するのが一般的であり、init()
の処理方法により、ファイル名を変更するとinit()
の処理順序が変わる場合があります。 これは、プログラムの出力を変更するという望ましくない影響を与える可能性があります。 再現性のある初期化動作を保証するために、ビルドシステムは、同じパッケージに属する複数のファイルを字句ファイル名の順序でコンパイラーに提示することをお勧めします。 すべてのinit()
関数が順番にロードされるようにする1つの方法は、それらすべてを1つのファイルで宣言することです。 これにより、ファイル名が変更されても順序が変更されなくなります。
init()
関数の順序が変更されないようにすることに加えて、グローバル変数、つまり、どこからでもアクセスできる変数を使用して、パッケージ内の状態を管理しないようにする必要があります。その包み。 前のプログラムでは、message.Message
変数がパッケージ全体で使用可能であり、プログラムの状態を維持していました。 このアクセスにより、init()
ステートメントは変数を変更し、プログラムの予測可能性を不安定にすることができました。 これを回避するには、プログラムの動作を許可しながら、アクセスが可能な限り少ない制御されたスペースで変数を操作するようにしてください。
1つのパッケージに複数のinit()
宣言を含めることができることを確認しました。 ただし、そうすると、望ましくない効果が生じ、プログラムが読みにくくなったり、予測しにくくなる可能性があります。 複数のinit()
ステートメントを回避するか、それらをすべて1つのファイルに保持することで、ファイルを移動したり名前を変更したりしてもプログラムの動作が変わらないようにします。
次に、init()
を使用して副作用のあるインポートを行う方法を調べます。
副作用にinit()
を使用する
Goでは、パッケージの内容ではなく、パッケージのインポート時に発生する副作用のために、パッケージをインポートすることが望ましい場合があります。 これは多くの場合、インポートされたコードにinit()
ステートメントがあり、他のコードの前に実行されることを意味します。これにより、開発者はプログラムの開始状態を操作できます。 この手法は、副作用のインポートと呼ばれます。
副作用のためにインポートする一般的な使用例は、コードに登録機能を使用することです。これにより、プログラムがコードのどの部分を使用する必要があるかをパッケージに通知できます。 たとえば、画像パッケージでは、image.Decode
関数は、デコードしようとしている画像の形式(jpg
、png
、[ X149X] など)実行する前に。 これを実現するには、最初にinit()
ステートメントの副作用がある特定のプログラムをインポートします。
次のコードスニペットを使用して、.png
ファイルでimage.Decode
を使用しようとしているとします。
サンプルデコードスニペット
. . . func decode(reader io.Reader) image.Rectangle { m, _, err := image.Decode(reader) if err != nil { log.Fatal(err) } return m.Bounds() } . . .
このコードを含むプログラムは引き続きコンパイルされますが、png
イメージをデコードしようとすると、エラーが発生します。
これを修正するには、最初にimage.Decode
の画像形式を登録する必要があります。 幸い、image/png
パッケージには、次のinit()
ステートメントが含まれています。
image / png / reader.go
func init() { image.RegisterFormat("png", pngHeader, Decode, DecodeConfig) }
したがって、image/png
をデコードスニペットにインポートすると、image/png
のimage.RegisterFormat()
関数がコードの前に実行されます。
サンプルデコードスニペット
. . . import _ "image/png" . . . func decode(reader io.Reader) image.Rectangle { m, _, err := image.Decode(reader) if err != nil { log.Fatal(err) } return m.Bounds() }
これにより、png
バージョンのimage.Decode()
が必要な状態とレジスタが設定されます。 この登録は、image/png
のインポートの副作用として発生します。
"image/png"
の前に空白の識別子(_
)があることに気づいたかもしれません。 これが必要なのは、Goではプログラム全体で使用されていないパッケージをインポートできないためです。 空白の識別子を含めることにより、インポート自体の値が破棄されるため、インポートの副作用のみが発生します。 これは、コードでimage/png
パッケージを呼び出さなくても、副作用のためにインポートできることを意味します。
副作用のためにパッケージをインポートする必要がある場合を知ることが重要です。 適切な登録がないと、プログラムはコンパイルされますが、実行時に正しく動作しない可能性があります。 標準ライブラリのパッケージは、ドキュメントでこのタイプのインポートの必要性を宣言します。 副作用のためにインポートが必要なパッケージを作成する場合は、使用しているinit()
ステートメントが文書化されていることを確認して、パッケージをインポートするユーザーが適切に使用できるようにする必要があります。
結論
このチュートリアルでは、init()
関数がパッケージ内の残りのコードが読み込まれる前に読み込まれること、および目的の状態の初期化など、パッケージの特定のタスクを実行できることを学びました。 また、コンパイラが複数のinit()
ステートメントを実行する順序は、コンパイラがソースファイルをロードする順序に依存することも学びました。 init()
の詳細については、公式の Golangドキュメントを確認するか、関数に関するGoコミュニティのディスカッションをお読みください。
関数の詳細については、 Go で関数を定義および呼び出す方法の記事を参照するか、Goシリーズのコーディング方法全体を参照してください。