Goのinit関数にどう向き合うか:構造設計とレビュー観点の整理

Goのinit()関数は、通常の関数とは異なるライフサイクルを持つ特別な関数として定義されています。
その性質上、開発者が意識しない状態変化や副作用を持ち込みやすく、レビューアーとしてはその挙動と設計意図を慎重に判断する必要があります。

この記事では、許容される設計/避けるべき設計の双方を通して、init()関数の適切な利用判断と、レビュー時に注視すべき観点を整理します。


適切なinit関数の使い方と設計の背景

まずは「これは問題ない」と判断できる、典型的で許容されるinit()関数の使用例からスタートします。

ログ設定や登録系の構造

// logflags.go
package config

import "log"

func init() {
	log.SetFlags(log.Lshortfile | log.Ldate)
}

このようなログ設定の初期化は、Go言語でも公式パターンとして広く許容されています。

  • 呼び出し元が意識せずに使える
  • 実行順序が外部影響しにくい
  • 再初期化の必要が基本的にない

という条件がそろっており、再利用性よりも自動化された初期構成が重要視される文脈です。


レジストリ系の構造におけるhook初期化

// driver.go
package driver

import "database/sql"

type Driver struct{}

func (d *Driver) Open(name string) (sql.Conn, error) {
	// ...
}

func init() {
	sql.Register("custom", &Driver{})
}

こちらも、Goの標準APIに設計されたinit()想定の構造です。
開発者が自前で呼び出すことなく、登録時点でhook的に初期化が完了する構造として設計されており、許容されやすいパターンといえます。


init()関数の主な許容パターン
  • ログ設定、グローバルフラグの初期化
  • レジストリ登録型(plugin, sql.Register等)
  • テストコード内での軽微な初期化

init関数が設計上問題となるケースと指摘コメント

ここからは、レビューアーが「これは見逃せない」と判断すべき構造を整理していきます。
すべて実コード形式で提示し、レビューツール上でも使えるようにコメントブロック内で指摘文を記載しています。


副作用を伴う初期化(例:ファイル削除)

func init() {
	os.RemoveAll("/tmp/config")
}
@Reviewer
init()内での破壊的操作はトレース不能で予測性がありません。関数化し、明示呼び出し可能な構造に変更してください。

初期化順序に依存した設計

// pkg1/config.go
var config = map[string]int{}

func init() {
	config["timeout"] = 30
}

// pkg2/main.go
func init() {
	fmt.Println(config["timeout"])
}
@Reviewer
複数パッケージのinit()実行順序に依存しています。将来的なリファクタリングで破綻する恐れがあるため、明示的な初期化APIに置き換えましょう。

テストの再現性を損ねる実装

func init() {
	seed = time.Now().UnixNano()
}
@Reviewer
時刻依存の初期化はテストの非決定性を生みます。setup関数等で明示的に制御できる構造に変更してください。

外部依存を含む初期化処理

func init() {
	resp, err := http.Get("https://example.com/init")
	if err != nil {
		panic(err)
	}
	globalConfig = parse(resp.Body)
}
@Reviewer
ネットワーク依存の初期化処理は、リトライ制御もトレースもできません。init()に持ち込むべきではありません。

init()で何をしているかが見えない構造は、設計者以外にとって“ブラックボックス”になりやすく、将来的な保守・拡張のリスクになります。


レビュー観点チェックリスト

観点 チェック内容
利用妥当性 init()である必然性があるか?他の構造で代替できないか?
副作用管理 副作用(ファイル削除、IO操作、状態変更)が暗黙に発生していないか?
初期化順序 他のパッケージやファイルの初期化順序に依存していないか?
テスト影響 テストの再現性・可視性を妨げていないか?
デバッグ性 予期しないタイミングでinit()が動作してトラブルの元になっていないか?
明示性の有無 他開発者にとって処理の意図・順序が明確か?ログ出力・コメントはあるか?

結論:init()は「どう使うか」より「なぜ使うか」を問うべき構造

Goのinit()は便利さの裏で、構造の透明性を下げやすい要素を多く含みます。
レビューアーの立場では、機能そのものより設計者の意図が明示されているか・他構造で置き換えられないかという点に重点を置くべきです。

特に以下のような方針を持つとよいでしょう。

  • テスト再現性に寄与しないinitは再検討
  • 副作用を含む場合は関数として分離
  • 順序依存は全廃方針でチェック
  • 「読めばわかる」ではなく「見ればわかる」構造へ

あとがき:init()は“必要悪”か?

init()が全否定されるべき機能ではないことは間違いありません。
ただし、「動くからOK」では済まない構造的リスクをはらんでいます。

レビューとはコードの“現在の正しさ”だけではなく、将来の読みやすさ・修正のしやすさ・設計意図の明確性を問う行為でもあります。
その中でinit()のような特殊構文は、「使わない理由がない」ではなく「使う必然性がある」かどうかで評価すべきです。

コードレビューの場でこの視点を共有できれば、init()の設計がブラックボックスになるリスクは大幅に下がるでしょう。