One Step Ahead

プログラミングやエンジニアリング全般について書いていきます

golangのinit関数は複数回定義できる

はじめに


そのまんまタイトル通りの内容ですw
Github で いくつかgolangのリポジトリをごそごそしている時に、init関数が1ファイルに対して、複数回定義されているのを発見しました。
Packageの初期化をするための関数という認識だったので、勝手に『1回だけの定義なんだろうな。』と思っていました。お恥ずかしい。
復習も含めて、再度init関数について調べなおしてみました。

init関数の役割


init関数は特殊な関数で、Packageの初期化を行う際に機能します。

package main

import (
    "fmt"
)

var message string

func init() {
    message = "Hello world"
}

func main() {
    fmt.Println(message)
}

main関数実行前の初期化として作用します。

init関数の呼び出し


init関数は、PackageをImportした際に呼び出されます。 あくまで、『importした際』に呼び出されるのがポイントです。
これを検証するために、下記のような構造のソースコードを用意しました。

src --- main.go
     |
     | --- lib1
     |      | --- lib.go
     |
     | --- lib2
            | --- lib.go
// /src/lib1/lib.go
package lib1

import "fmt"

var libName = "lib1"

func init() {
    fmt.Printf("%s init called!!\r\n", libName)
}

func Func() {
    fmt.Printf("%s Func Done.\r\n", libName)
}
// /src/lib2/lib.go
package lib2

import "fmt"

var libName = "lib2"

func init() {
    fmt.Printf("%s init called!!\r\n", libName)
}

func Func() {
    fmt.Printf("%s Func Done.\r\n", libName)
}

Packageに相当する2つのファイルでは、それぞれinit関数を定義しており、呼び出しのタイミングでConsole上に文字を出力しています。

/src/main.go
package main

import (
    "fmt"

    "github.com/atEaE/go-sample/lib2"
    "github.com/atEaE/go-sample/lib1"
)

func init() {
    fmt.Println("main called")
}

func main() {
    lib1.Func()
    lib2.Func()
    fmt.Println("Hello world!")
}

main.goでは、2つのPackageを参照し、それぞれmain関数の中でPackageの関数を呼び出しています。 ここでは呼び出し順を確認するために、下記ような順番でソースコードを記載しています。

  1. lib2をImportする.
  2. lib1をImportする.
  3. lib1内のFunc関数を呼び出す。
  4. lib2内のFunc関数を呼び出す。

※ VSCode でgofmtなどをしていると保存の際に自動的に並び順がフォーマットされてしまうので、あえてメモ帳などを使って編集しています。

なんとなく(私だけかも知れないが...)、main関数内の記載順に呼びされそうな感じがしますが、上のコードを実行するとimport順にinit関数が呼びされることが確認できます。

C:\workspace\sample\go-sample> go run main.go
lib2 init called!!
lib1 init called!!
main called
lib1 Func Done.
lib2 Func Done.
Hello world!

加えて、呼び出し元であるmain.goinit関数はimport終了後に呼びされているのが確認できます。

init関数の定義


init関数は1ファイルに限らず、Package内で複数回定義ができます。
これについては、公式のドキュメント内の『Package initialization』の項に記載がありました。

Multiple such functions may be defined per package, even within a single source file.
--- このような関数(init関数)は、1つのソースファイル内であっても、パッケージごとに複数定義することができます。--- The Go Programming Language Specification

これも検証用のソースコードを用意しました。

src --- main.go
     |
     | --- lib
     |      | --- lib1.go
            | --- lib2.go
// /src/lib/lib1.go
package lib

import "fmt"

var multiPle1 string

func init() {
    multiPle1 = "lib1 init called"
}

func init() {
    fmt.Printf("%s\r\n", multiPle1)
}

func Hello() {
    fmt.Print("Hello, ")
}
// /src/lib/lib2.go
package lib

import "fmt"

var multiPle2 string

func init() {
    multiPle2 = "lib2 init called"
}

func init() {
    fmt.Printf("%s\r\n", multiPle2)
}

func World() {
    fmt.Print("World!!")
}

libPackage全体でみると4回のinit関数が定義されており、各ファイルで2回ずつinit関数を定義しています。

// /src/main.go
package main

import "github.com/atEaE/go-sample/lib"

func main() {
    lib.Hello()
    lib.World()
}

このコードの事項結果は下記のようになります。

C:\workspace\sample\go-sample> go run main.go
lib1 init called
lib2 init called
Hello, World!!

初期化処理が煩雑な場合などは、initを分割した方がメンテナンスが高くなりそうですね。

まとめ


  • init関数はimport時に呼び出される。
  • init関数は1ファイルに限らず, Package毎に複数回定義できる。