開発者ドキュメント

Goでのinitの理解

序章

Goでは、事前定義されたinit()関数が、パッケージの他の部分の前に実行されるコードの一部を開始します。 このコードは、パッケージがインポートされるとすぐに実行され、特定の構成やリソースのセットがある場合など、アプリケーションを特定の状態で初期化する必要がある場合に使用できます。開始する必要があります。 また、副作用をインポートするときにも使用されます。これは、特定のパッケージをインポートしてプログラムの状態を設定するために使用される手法です。 これは、プログラムがタスクの正しいコードを検討していることを確認するために、あるパッケージを別のパッケージとregisterするためによく使用されます。

init()は便利なツールですが、見つけにくいinit()インスタンスはコードの実行順序に大きく影響するため、コードが読みにくくなる場合があります。 このため、Goを初めて使用する開発者は、この関数の側面を理解して、コードを作成するときにinit()を読みやすく使用できるようにすることが重要です。

このチュートリアルでは、init()が特定のパッケージ変数のセットアップと初期化、1回限りの計算、および別のパッケージで使用するパッケージの登録にどのように使用されるかを学習します。

前提条件

この記事のいくつかの例では、次のものが必要になります。

.
├── 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の値は空の文字列です。

このコードを実行してみましょう:

  1. go run main.go

weekdayの値が空白であるため、プログラムを実行すると、次の出力が得られます。

Output
Today 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を初期化しました。その値。

プログラムを実行すると、現在の平日が出力されます。

Output
Today is Monday

これはinit()がどのように機能するかを示していますが、init()のより一般的な使用例は、パッケージをインポートするときに使用することです。 これは、パッケージを使用する前に、パッケージで特定のセットアップタスクを実行する必要がある場合に役立ちます。 これを実証するために、パッケージが意図したとおりに機能するために特定の初期化を必要とするプログラムを作成しましょう。

インポート時にパッケージを初期化する

まず、スライスからランダムなクリーチャーを選択して出力するコードを記述します。 ただし、最初のプログラムではinit()を使用しません。 これにより、私たちが抱えている問題と、init()がどのように問題を解決するかがわかりやすくなります。

src/github.com/gopherguides/ディレクトリ内から、次のコマンドを使用してcreatureというフォルダを作成します。

  1. mkdir creature

creatureフォルダー内に、creature.goというファイルを作成します。

  1. 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フォルダーを作成します。

  1. mkdir cmd

cmdフォルダー内に、main.goというファイルを作成します。

  1. 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という名前のファイルを作成します。

  1. 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ファイルを作成します。

  1. 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プログラムを実行できます。

  1. go run cmd/main.go

これにより、次のようになります。

Output
jellyfish squid squid dolphin

このプログラムを実行すると、4つの値を受け取り、それらを印刷しました。 プログラムを複数回実行すると、常にが、期待どおりのランダムな結果ではなく、同じ出力を取得することに気付くでしょう。 これは、randパッケージが疑似乱数を作成し、単一の初期状態に対して同じ出力を一貫して生成するためです。 より乱数を実現するには、パッケージをシードするか、プログラムを実行するたびに初期状態が異なるようにソースを変更するように設定します。 Goでは、現在の時刻を使用してrandパッケージをシードするのが一般的です。

creatureパッケージでランダム機能を処理する必要があるため、次のファイルを開きます。

  1. 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()を使用して現在の時刻をシードしました。 ファイルを保存して終了します。

これで、プログラムを実行すると、ランダムな結果が得られます。

  1. go run cmd/main.go
Output
jellyfish octopus shark jellyfish

プログラムを何度も実行し続けると、ランダムな結果が得られ続けます。 ただし、これはまだコードの理想的な実装ではありません。creature.Random()が呼び出されるたびに、rand.Seed(time.Now().UnixNano())を再度呼び出すことによってrandパッケージも再シードするためです。 再シードすると、内部クロックが変更されていない場合に同じ初期値でシードされる可能性が高くなり、ランダムパターンが繰り返される可能性があります。または、プログラムがクロックの変更を待機することにより、CPU処理時間が長くなります。

これを修正するには、init()関数を使用できます。 creature.goファイルを更新しましょう。

  1. 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回実行して、乱数生成用の単一シードを提供する必要があることをコンパイラーに通知します。 これにより、必要以上にコードを実行しないようになります。 プログラムを実行すると、ランダムな結果が得られます。

  1. go run cmd/main.go
Output
dolphin 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() {}

次のコマンドでプログラムを実行すると、次のようになります。

  1. go run main.go

次の出力が表示されます。

Output
First 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ディレクトリから次のコマンドを実行します。

  1. go run *.go

cmdフォルダーにはmainパッケージを構成する複数のGoファイルがあるため、cmd内のすべての.goファイルをコンパイラーに通知する必要があります。 ]フォルダをコンパイルする必要があります。 *.goを使用すると、.goで終わるcmdフォルダー内のすべてのファイルをロードするようにコンパイラーに指示されます。 go run main.goのコマンドを発行した場合、a.goおよびb.goファイルのコードが表示されないため、プログラムはコンパイルに失敗します。

これにより、次の出力が得られます。

Output
a -> b -> Hello

Package Initialization のGo言語仕様に従って、パッケージ内で複数のファイルが検出されると、それらはアルファベット順に処理されます。 このため、a.goからmessage.Messageを初めて印刷したときは、値が空白でした。 b.goinit()関数が実行されるまで、値は初期化されませんでした。

a.goのファイル名をc.goに変更すると、異なる結果が得られます。

Output
b -> Hello a -> Hello

これで、コンパイラは最初にb.goに遭遇するため、c.goinit()関数の場合、message.Messageの値はHelloですでに初期化されています。 ]に遭遇しました。

この動作により、コードに問題が発生する可能性があります。 ソフトウェア開発ではファイル名を変更するのが一般的であり、init()の処理方法により、ファイル名を変更するとinit()の処理順序が変わる場合があります。 これは、プログラムの出力を変更するという望ましくない影響を与える可能性があります。 再現性のある初期化動作を保証するために、ビルドシステムは、同じパッケージに属する複数のファイルを字句ファイル名の順序でコンパイラーに提示することをお勧めします。 すべてのinit()関数が順番にロードされるようにする1つの方法は、それらすべてを1つのファイルで宣言することです。 これにより、ファイル名が変更されても順序が変更されなくなります。

init()関数の順序が変更されないようにすることに加えて、グローバル変数、つまり、どこからでもアクセスできる変数を使用して、パッケージ内の状態を管理しないようにする必要があります。その包み。 前のプログラムでは、message.Message変数がパッケージ全体で使用可能であり、プログラムの状態を維持していました。 このアクセスにより、init()ステートメントは変数を変更し、プログラムの予測可能性を不安定にすることができました。 これを回避するには、プログラムの動作を許可しながら、アクセスが可能な限り少ない制御されたスペースで変数を操作するようにしてください。

1つのパッケージに複数のinit()宣言を含めることができることを確認しました。 ただし、そうすると、望ましくない効果が生じ、プログラムが読みにくくなったり、予測しにくくなる可能性があります。 複数のinit()ステートメントを回避するか、それらをすべて1つのファイルに保持することで、ファイルを移動したり名前を変更したりしてもプログラムの動作が変わらないようにします。

次に、init()を使用して副作用のあるインポートを行う方法を調べます。

副作用にinit()を使用する

Goでは、パッケージの内容ではなく、パッケージのインポート時に発生する副作用のために、パッケージをインポートすることが望ましい場合があります。 これは多くの場合、インポートされたコードにinit()ステートメントがあり、他のコードの前に実行されることを意味します。これにより、開発者はプログラムの開始状態を操作できます。 この手法は、副作用のインポートと呼ばれます。

副作用のためにインポートする一般的な使用例は、コードに登録機能を使用することです。これにより、プログラムがコードのどの部分を使用する必要があるかをパッケージに知らせることができます。 たとえば、画像パッケージでは、image.Decode関数は、デコードしようとしている画像の形式(jpgpng、[ X149X]

モバイルバージョンを終了