[Go] 構造体フィールドの初期化漏れをCIで防ぐ!~exhaustruct~

2024/07/29に公開

はじめに

Go言語では、構造体を使ってデータを定義することがよくあります。
しかし、構造体フィールドの初期化漏れは、意図しないバグを引き起こすことがあります。
本記事では、構造体フィールドの初期化漏れを防ぐツール「exhaustruct」を紹介し、その使い方を解説します。

構造体フィールドの初期化漏れの問題

Go言語で構造体を使用する際、全てのフィールドを適切に初期化しないと、予期しない動作を引き起こす可能性があります。
例えば、以下のような構造体があるとします。

type Item struct {
	itemType int
	name string
}

この構造体を初期化する際、もしフィールドの一部を初期化し忘れると、未初期化のフィールドはゼロ値を持つことになります。これは意図せずバグを生む原因となります。

func main() {
	item1 := Item{itemType: 1, name: "a"} // {itemType:1 name:a}
	item2 := Item{name: "b"}              // {itemType:0 name:b} ゼロ値を持つ
}

この対応としてよく行われるのは、コンストラクタ関数を用意して初期化を一点に集めることです。例えば、以下のようにします。

func NewItem(itemType int, name string) Item {
    return Item{
        itemType: itemType,
        name:     name,
    }
}

func main() {
	item1 := NewItem(1, "a") // {itemType:1 name:a}
	item2 := NewItem(2, "b") // {itemType:2 name:b} 引数で入力を強制
}

しかし、プロジェクトが大規模になると、新しいフィールドが追加されたときにコンストラクタ関数の更新を見落とすことがあります。このため、初期化漏れの問題が依然として発生するリスクがあります。

exhaustructの概要

exhaustructは、構造体の全フィールドが確実に初期化されているかをチェックするツールです。これにより、初期化漏れを防ぐことができます。
https://github.com/GaijinEntertainment/go-exhaustruct
さらに、exhaustructgolangci-lintにも組み込まれているため、既にgolangci-lintを使用しているプロジェクトではすぐに利用可能です。

golangci-lintでの設定方法

既にgolangci-lintを利用している場合、以下の設定を追加することでexhaustructを有効にできます。

golangci-lint設定ファイル
linters:
  enable:
    - exhaustruct

これにより、プロジェクト内の全ての構造体が初期化されているかをチェックし、未初期化のフィールドがある場合は警告が表示されます。

実際に使ってみる

先ほどの例でexhaustructの使い方を説明します。

package main

func main() {
	item1 := Item{itemType: 1, name: "a"}
	item2 := Item{name: "b"}
	item3 := Item{}
}

こちらのコードに対してgolangci-lintを実行すると、出力結果として、未初期化のフィールドがある場合は警告が表示されます。

$ golangci-lint run
main.go:12:11: main.Item is missing field itemType (exhaustruct)
        item2 := Item{name: "b"}
                 ^
main.go:13:11: main.Item is missing fields itemType, name (exhaustruct)
        item3 := Item{}
                 ^

カスタマイズ

exhaustructは特定の構造体やフィールドを無視する設定ができます。

特定構造体のチェックを除外

exhaustructは設定ファイルを使って、特定の構造体のチェックを無視できます。

linters-settings:
  exhaustruct:
    # 構造体のパッケージおよび名前に一致する正規表現のリスト。
    # 正規表現はパッケージ/名前/構造体名に一致する必要があります。
    # このリストが空の場合、すべての構造体がテストされます。
    # デフォルト: []
    include:
      - '.+\.Test'
      - 'example\.com/package\.ExampleStruct[\d]{1,2}'
    # チェックから除外する構造体パッケージおよび名前に一致する正規表現のリスト。
    # 正規表現はパッケージ/名前/構造体名に一致する必要があります。
    # デフォルト: []
    exclude:
      - '.+/cobra\.Command$'

私が所属するプロジェクトでは、GraphQLの自動生成モデルにおいて初期化漏れが発生しやすいという課題がありました。
また、意図的にゼロ値を使用しているコードも存在するため、まずはGraphQLの自動生成モデルに限定してexhaustructを適用することにしました。

linters-settings:
  exhaustruct:
    include:
      - "path/to/graphql/models/..."

特定フィールドのチェックを除外

特定のフィールドを無視する場合、構造体のタグに exhaustruct:"optional" を設定することができます。例えば、以下のようにします。

type Item struct {
	itemType int `exhaustruct:"optional"` // このフィールドはチェック対象外
	name string                           // このフィールドはチェック対象
}

このタグを付けることで、exhaustructはそのフィールドの初期化漏れを無視します。

まとめ

構造体フィールドの初期化漏れは、Go言語のプロジェクトにおいて避けたい問題の一つです。
exhaustructを使うことで、これらの問題を効果的に防ぐことができます。
さらに、golangci-lintと統合することで、既存のLintingプロセスに簡単に追加できるため、導入が非常にスムーズです。
みなさんもぜひ、exhaustructを導入して、バグ予防に活用してみてください。

Hacobell Developers Blog

Discussion