nilnil

Table of Contents

  1. 解決する問題
  2. 設定
  3. オプション
    1. checked-types の種類
  4. サンプル
    1. 検出例
    2. 修正例
  5. 注意点
  6. 参考リンク

nilnil は nil エラーと無効な値(nil ポインタ等)を同時に返しているパターンを検出するツールです。

解決する問題

Go の慣例では「エラーが nil なら戻り値は有効」と期待されます。return nil, nil を返すと呼び出し側は nil チェックを忘れてパニックを起こすリスクがあります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Before: nil, nil を返す → 呼び出し側が nil チェックを忘れるとパニック
func FindUser(ctx context.Context, id string) (*User, error) {
row := db.QueryRow(ctx, "SELECT * FROM users WHERE id = $1", id)

var user User
if err := row.Scan(&user.ID, &user.Name); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, nil // ユーザーが見つからない → nil, nil
}
return nil, fmt.Errorf("scan user: %w", err)
}
return &user, nil
}

// 呼び出し側
user, err := FindUser(ctx, "123")
if err != nil {
return err
}
fmt.Println(user.Name) // user が nil ならパニック!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// After: センチネルエラーを返す
var ErrUserNotFound = errors.New("user not found")

func FindUser(ctx context.Context, id string) (*User, error) {
row := db.QueryRow(ctx, "SELECT * FROM users WHERE id = $1", id)

var user User
if err := row.Scan(&user.ID, &user.Name); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrUserNotFound // 明示的なエラーを返す
}
return nil, fmt.Errorf("scan user: %w", err)
}
return &user, nil
}

設定

公式ドキュメント

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# .golangci.yml 設定例
version: "2"

linters:
enable:
- nilnil
settings:
nilnil:
checked-types:
- ptr
- func
- iface
- map
- chan

オプション

オプション デフォルト 説明
checked-types []string [ptr, func, iface, map, chan] チェック対象の型
only-two bool true 戻り値が 2 つの関数のみチェックする。false にすると 3 つ以上でもチェック
detect-opposite bool false 逆パターン(非 nil エラーと有効な値の同時返却)も検出する

checked-types の種類

説明 nil 時のリスク
ptr ポインタ nil pointer dereference
func 関数値 nil pointer dereference
iface インターフェース nil pointer dereference
map マップ assignment to entry in nil map
chan チャネル deadlock
uintptr uintptr invalid memory access
unsafeptr unsafe.Pointer invalid memory access

サンプル

検出例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// nilnil が警告するパターン

// 1. ポインタと error
func GetConfig(name string) (*Config, error) {
if name == "" {
return nil, nil // return both nil error and nil pointer
}
return loadConfig(name)
}

// 2. インターフェースと error
func NewHandler(kind string) (http.Handler, error) {
if kind == "unknown" {
return nil, nil // return both nil error and nil interface
}
return createHandler(kind)
}

// 3. map と error
func LoadSettings(path string) (map[string]string, error) {
if path == "" {
return nil, nil // return both nil error and nil map
}
return parseSettings(path)
}

修正例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var ErrConfigNotFound = errors.New("config not found")

func GetConfig(name string) (*Config, error) {
if name == "" {
return nil, ErrConfigNotFound
}
return loadConfig(name)
}

var ErrUnknownHandler = errors.New("unknown handler kind")

func NewHandler(kind string) (http.Handler, error) {
if kind == "unknown" {
return nil, ErrUnknownHandler
}
return createHandler(kind)
}

func LoadSettings(path string) (map[string]string, error) {
if path == "" {
return nil, errors.New("settings path required")
}
return parseSettings(path)
}

注意点

  • detect-opposite: true にすると、エラーが非 nil なのに有効な値も返すパターンも検出する。io.ReadAll のように部分的な結果とエラーを同時に返す正当なケースもあるため、誤検知に注意
  • only-two: false にすると 3 つ以上の戻り値を持つ関数もチェック対象になる
  • Go の慣例に従い、エンティティが見つからない場合はセンチネルエラー(var ErrNotFound = errors.New(...))を返す設計にする

参考リンク