Help us understand the problem. What is going on with this article?

init関数のふしぎ #golang

More than 1 year has passed since last update.

はじめに

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

tenntenn
Go engineer / Gopher artist
mercari
フリマアプリ「メルカリ」を、グローバルで開発しています。
https://tech.mercari.com/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした