Goのinitを理解する

前書き

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

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

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

前提条件

この記事の一部の例では、次のものが必要です。

.
├── 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と呼ばれるグローバルvariableを宣言しました。 デフォルトでは、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()のより一般的な使用例は、パッケージをインポートするときに使用することです。 これは、パッケージを使用する前にパッケージ内の特定のセットアップタスクを実行する必要がある場合に役立ちます。 これを実証するために、パッケージが意図したとおりに機能するために特定の初期化を必要とするプログラムを作成しましょう。

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

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

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

mkdir creature

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

nano creature/creature.go

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

creature.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と呼ばれる変数を定義します。 また、creatures変数からランダムな値を返すexportedRandom関数もあります。

このファイルを保存して終了します。

次に、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

ファイルに次のコード行を追加します。

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

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

nano creature/creature.go

次の強調表示された行をcreature.goファイルに追加します。

creature/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/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()+`s can make it difficult to know which one has priority over the others. In this section, we will show how to maintain control over multiple `+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

mainパッケージを構成するcmdフォルダーに複数の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.goinit()関数が実行されるまで初期化されませんでした。

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

Outputb -> Hello
a -> Hello

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

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

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

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

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

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

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

副作用のためにインポートする一般的な使用例は、コード内のregister機能です。これにより、プログラムがコードのどの部分を使用する必要があるかをパッケージに通知します。 たとえば、image packageでは、image.Decode関数は、デコードしようとしている画像の形式(jpgpnggifなど)を知る必要があります。 。)実行する前に。 これを実現するには、最初に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/pngimage.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()
}

これにより、image.Decode()pngバージョンが必要であるという状態とレジスタが設定されます。 この登録は、image/pngのインポートの副作用として発生します。

"image/png"の前にblank identifier_)があることに気づいたかもしれません。 Goではプログラム全体で使用されていないパッケージをインポートできないため、これが必要です。 空白の識別子を含めると、インポート自体の値が破棄されるため、インポートの副作用のみが発生します。 つまり、コードでimage/pngパッケージを呼び出すことはありませんが、副作用のためにインポートすることができます。

副作用のためにいつパッケージをインポートする必要があるかを知ることは重要です。 適切に登録しないと、プログラムはコンパイルされますが、実行時に正しく機能しなくなる可能性があります。 標準ライブラリのパッケージは、ドキュメントでこのタイプのインポートの必要性を宣言します。 副作用のためにインポートが必要なパッケージを作成する場合は、使用しているinit()ステートメントが文書化されていることを確認して、パッケージをインポートするユーザーがそれを適切に使用できるようにする必要があります。

結論

このチュートリアルでは、パッケージ内の残りのコードがロードされる前にinit()関数がロードされること、および目的の状態の初期化など、パッケージに対して特定のタスクを実行できることを学びました。 また、コンパイラが複数のinit()ステートメントを実行する順序は、コンパイラがソースファイルをロードする順序に依存することも学びました。 init()について詳しく知りたい場合は、公式のGolang documentationを確認するか、the discussion in the Go community about the functionを読んでください。

関数の詳細については、How To Define and Call Functions in Goの記事を参照するか、the entire How To Code in Go seriesを調べてください。