はじめに
Go言語のinit関数についてまとめました。
init関数とは?
アプリケーションの状態を初期化するために用いられる関数。パッケージ変数の初期化などに使われる。引数を取らず、何も値を返さない。パッケージが初期化→パッケージ内の全ての定数と変数宣言が評価→init関数の実行。
// 2番目に実行
func init() {
fmt.Println("init")
}
// 一番最初に実行
var a = func() int {
fmt.Println("var")
return 0
}()
// 最後に実行
func main() {
fmt.Println("main")
}
これを実行すると、以下のようになります。
var
init
main
他にも、実際の実行が始まる前にプログラムの状態が正しいかどうかを検証または修復する用途にも使用される。
func init() {
if user == "" {
log.Fatal("$USER not set")
}
if home == "" {
home = "/home/" + user
}
if gopath == "" {
gopath = home + "/go"
}
// gopath may be overridden by --gopath flag on command line.
flag.StringVar(&gopath, "gopath", gopath, "デフォルトのGOPATHをオーバライドする")
}
mainが別パッケージに依存する場合
パッケージ内に複数のinit関数がある場合
ファイルのアルファベット順に実行される。
a.goのinit() → b.goのinit()の順に実行される。
ファイル名が変更されると、init()の実行順が変更される可能性があるので、init()の順序に依存してはいけない。
下図は複数パッケージの場合の実行順例。
副作用のためのinit()
他パッケージのinit()だけを実行したい場合、importでブランク識別子_
をつける。
import (
"fmt"
_ "foo"
)
ちなみにmain関数の中でinit関数は実行できない。
func main() {
init() // コンパイルエラー
}
init()を使ってはいけないとき
データベースのコネクションプールの保持
データベースをオープンして、Pingができるか確認し、グローバル変数に代入している。
var db *sql.DB
func init() {
dataSourceName := os.Getenv("MYSQL_DATA_SOURCE_NAME")
d, err := sql.Open("mysql", dataSourceName)
if err != nil {
log.Panic(err)
}
err = d.Ping()
if err != nil {
log.Panic(err)
}
db = d
}
上記は以下3つの問題がある。
- init関数はエラーを返さない
- パニックを起こし、アプリケーションを停止することでエラーを通知する。呼び出し元でリトライやフォールバックができなくなる
- テストケースを実行する前にinit関数が実行される
- DBへのコネクションを作成する必要ないユーティリティ関数に対する単体テストなど、テストが書きにくくなる
- データベースのコネクションプールをグローバル変数に代入する必要がある
- 変数は、グローバル変数よりカプセル化するべき
- どの関数からでもパッケージ内のグローバル変数を変更できてしまう
- グローバル変数に依存する関数は切り離されないので、単体テストが複雑になる可能性がある
- 変数は、グローバル変数よりカプセル化するべき
先ほどのコードは、以下のように改善できる。
func NewDbClient(dsn string) (*sql.DB, error) {
db, err := sql.Open("mysql", dsn)
if err != nil {
return nil, err
}
if err = db.Ping(); err != nil {
return nil, err
}
db.SetConnMaxLifetime(15 * time.Second)
db.SetMaxIdleConns(10)
db.SetMaxOpenConns(10)
return db, nil
}
init()を使っていいとき
Goの公式ブログでは、以下のように静的なHTTP設定を行うためにinit関数を使用している。
func init() {
// Redirect "/blog/" to "/", because the menu bar link is to "/blog/"
// but we're serving from the root.
redirect := func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/", http.StatusFound)
}
http.HandleFunc("/blog", redirect)
http.HandleFunc("/blog/", redirect)
// Keep these static file handlers in sync with app.yaml.
static := http.FileServer(http.Dir("static"))
http.Handle("/favicon.ico", static)
http.Handle("/fonts.css", static)
http.Handle("/fonts/", static)
http.Handle("/lib/godoc/", http.StripPrefix("/lib/godoc/", http.HandlerFunc(staticHandler)))
}
このとき、以下3つを満たすので、上記のinit関数は有用である。
- initが失敗することはない(handlerがnilではないので、パニックにならない)
- グローバル変数を作る必要がない
- 単体テストに影響を与えることがない