Goのインターフェース値の内部構造とnilバグの罠
概要¶
Go のインターフェース値は内部的に (type, value) の2つのポインタで構成されている。この仕組みを理解していないと、「nil を代入したはずなのに nil でない」という直感に反するバグを生み出す。Go を書くなら必須の知識。
詳細¶
インターフェース値の内部構造¶
interface{}
┌─────────────┬─────────────┐
│ type ptr │ value ptr │
│ (*T) │ (*v) │
└─────────────┴─────────────┘
インターフェース値が nil になるのは、type と value が両方 nil のとき だけ。
nil interface vs interface containing nil¶
package main
import "fmt"
type MyError struct {
msg string
}
func (e *MyError) Error() string {
return e.msg
}
// BAD: *MyError の nil を error インターフェースに包んで返す
func badFunction() error {
var err *MyError = nil
return err // type=*MyError, value=nil → インターフェースとしては nil ではない!
}
// GOOD: nil を直接返す
func goodFunction() error {
return nil // type=nil, value=nil → インターフェースとして nil
}
func main() {
err1 := badFunction()
fmt.Println(err1 == nil) // false ← 罠!
fmt.Println(err1) // <nil> ← 表示は <nil> なのに...
err2 := goodFunction()
fmt.Println(err2 == nil) // true
}
よくあるバグパターン¶
// 条件付きで具体型の nil を返してしまうパターン
func process(input string) error {
var customErr *ValidationError
if input == "" {
customErr = &ValidationError{msg: "empty input"}
}
if customErr != nil {
return customErr
}
// ここで return nil とすべきだが...
return customErr // input が空でない場合、*ValidationError(nil) を返す → バグ!
}
正しい判定方法¶
import "reflect"
// インターフェースが「実質的に nil」かどうかを安全に判定
func isNil(i interface{}) bool {
if i == nil {
return true
}
v := reflect.ValueOf(i)
switch v.Kind() {
case reflect.Ptr, reflect.Interface, reflect.Slice,
reflect.Map, reflect.Chan, reflect.Func:
return v.IsNil()
}
return false
}
まとめ表¶
| 状態 | type | value | == nil |
|---|---|---|---|
var e error |
nil | nil | true |
var p *MyError; var e error = p |
*MyError |
nil | false |
e = &MyError{} |
*MyError |
0x... | false |
なぜ重要か / いつ使うか¶
- エラーハンドリングで具体型のポインタを返す関数を書くとき、必ず
return nilと書くべき理由がわかる if err != nilチェックが期待通り動かないバグの原因調査に直結する- Go の面接でよく問われる内部実装の知識として重要
- インターフェースを多用するライブラリ設計(特にモック・テスト)で安全なコードを書くために必須