init関数のふしぎ #golang

December 5, 2016

この記事はQiitaの記事をエクスポートしたものです。内容が古くなっている可能性があります。

はじめに

知られているようで、案外知られてないことを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関数についてまとめてみました。 ちなみに、この記事を書こうと思った元ネタはこちらです。これも面白いので、ぜひ読んでみて下さい!