init関数のふしぎ #golang

  • 32
    いいね
  • 0
    コメント

はじめに

知られているようで、案外知られてないことをQiitaに投稿していくのも良いと思い、軽いネタですがinit関数についてまとめたいと思います。

init関数とは

init関数は特殊な関数で、パッケージの初期化に使われます。
以下のようにmainパッケージに書くとmain関数より先に実行されます。
mainパッケージでない場合は、importするだけで呼び出されます。

package main

import (
    "fmt"
)

func init() {
    fmt.Println("hello, init")
}

func main() {
    fmt.Println("Hello, main")
}

Playgroundで動かす

init関数は何に使うのか?

上記では、初期化に使うと書きました。名前からしてそれは想像できますよね。
init関数は基本的にはパッケージ変数の初期化に用いられます。なぜならば、何かしらの副作用をパッケージに変数に与えなければ、呼んでも意味がないからです。もちろん、外部のリソースに何かしらの副作用を与える場合(サーバに何かリクエストしたり、ファイルに書き込んだり)もありますが、基本的にはパッケージ変数の初期化に使われます。

パッケージ変数は、代入文でも初期化を行うことがありますが、以下のような場合にinit関数を使います。

  • 代入文で表現できない初期化(forを使うなど)
    • もちろん、init関数でやらず、別の関数を呼び出してそれを代入してもよい
  • 初期化処理が複雑
  • 他のパッケージのパッケージ変数を初期化してやる
    • image/pngimage/jpegなどでやっている
  • main関数が使えない場合にエントリーポイントして使う
    • GAE/Goはmain関数を作らず、init関数をエントリーポイントとして使用する

init関数はいつ呼ばれるのか

以下のコードを実行すると、出力はどうなるでしょうか?
Hello, playgroundと出るのはなんとなく空気を読めばわかりますが、よく見るとパッケージ変数の初期化の方が先に実行されていることが分かります。
また、もちろんmain関数より先に実行されています。

package main

import (
    "fmt"
)

var msg = message()

func message() string {
    return "Hello"
}

func init() {
    fmt.Print(msg)
}

func main() {
    fmt.Println(", playground")
}

Playgroundで動かす

言語仕様を読む限り、依存するパッケージがある場合(import文がある場合)には、それらのパッケージの初期化が終わった後に自身のパッケージのinit関数が実行されるようです。

init関数はいくつ書けるのか?

よくよく考えると、複数ファイルにまたがって、1つのパッケージに複数のinit関数を書いていることがあるのに気づくことがあります。他の関数の場合は、複数定義すると、もちろんコンパイルエラーになります。

ちなみに、ファイルに1つという決まりもなく、複数書けるようになっています。

package main

import (
    "fmt"
)

func init() {
    fmt.Print("hello")
}

func init() {
    fmt.Println(", init")
}

func main() {
}

Playgroundで動かす

不思議ですね。runtimeパッケージを使って関数名がどうなっているのか調べてみましょう。

package main

import (
    "fmt"
    "runtime"
)

func init() {
    var pcs [1]uintptr
    runtime.Callers(1, pcs[:])
    fn := runtime.FuncForPC(pcs[0])
    fmt.Println(fn.Name())
}

func init() {
    var pcs [1]uintptr
    runtime.Callers(1, pcs[:])
    fn := runtime.FuncForPC(pcs[0])
    fmt.Println(fn.Name())
}

func main() {
    var pcs [1]uintptr
    runtime.Callers(1, pcs[:])
    fn := runtime.FuncForPC(pcs[0])
    fmt.Println(fn.Name())
}

Playgroundで動かす

実行すると、以下のように出力されます。

main.init.1
main.init.2
main.main

どうやら、各init関数には番号が付けられており、区別されているようです。

init関数は呼び出せるのか

init関数は他の関数内から呼び出せるんでしょうか?
main関数から呼び出してみます。

package main

import "fmt"

func init() {
    fmt.Println("hoge")
}

func main() {
    init()
}

Playgroundで動かす

実行すると、以下のようにエラーがでます。

tmp/sandbox671239315/main.go:10: undefined: init

定義されていないことになるようです。なるほど!

おわりに

init関数についてまとめてみました。
ちなみに、この記事を書こうと思った元ネタはこちらです。これも面白いので、ぜひ読んでみて下さい!

この投稿は Okinawa.go Advent Calendar 20164日目の記事です。