Go/nil

nilはGoの事前宣言された識別子(predeclared identifier)で、以下の型のゼロ値を表します:

  • ポインタ型
  • 関数型
  • インターフェース型
  • スライス型
  • マップ型
  • チャネル型

各型でのnilの使用例

1. ポインタ型

var p *int
if p == nil {
    fmt.Println("ポインタはnilです")
}

// nilポインタの逆参照はpanicを引き起こす
// fmt.Println(*p) // panic: runtime error

2. 関数型

var f func()
if f == nil {
    fmt.Println("関数はnilです")
}

// nil関数の呼び出しはpanicを引き起こす
// f() // panic: runtime error

3. インターフェース型

var i interface{}
if i == nil {
    fmt.Println("インターフェースはnilです")
}

// typed nilの場合
var err error
var p *int
err = p // typed nil
if err == nil {
    fmt.Println("これは実行されない") // errは非nilになる
}

4. スライス型

var s []int
if s == nil {
    fmt.Println("スライスはnilです")
}

// nilスライスでも一部の操作は可能
fmt.Println(len(s))    // 0
fmt.Println(cap(s))    // 0
// s = append(s, 1)    // これは動作する

5. マップ型

var m map[string]int
if m == nil {
    fmt.Println("マップはnilです")
}

// nilマップからの読み取りは可能(ゼロ値を返す)
value := m["key"] // 0

// nilマップへの書き込みはpanicを引き起こす
// m["key"] = 1 // panic: assignment to entry in nil map

6. チャネル型

var ch chan int
if ch == nil {
    fmt.Println("チャネルはnilです")
}

// nilチャネルでの操作は永続的にブロック
// <-ch    // 永続的にブロック
// ch <- 1 // 永続的にブロック

nilの比較と判定

等価比較

var p1, p2 *int
fmt.Println(p1 == p2)    // true(両方ともnil)
fmt.Println(p1 == nil)   // true

var s1, s2 []int
fmt.Println(s1 == nil)   // true
// fmt.Println(s1 == s2) // コンパイルエラー:スライス同士は比較不可

reflect.Valueまたはreflectパッケージでの判定

import "reflect"

func isNil(i interface{}) bool {
    if i == nil {
        return true
    }
    v := reflect.ValueOf(i)
    switch v.Kind() {
    case reflect.Ptr, reflect.Map, reflect.Array, reflect.Chan, reflect.Slice:
        return v.IsNil()
    }
    return false
}

nilとゼロ値の区別

// 構造体のゼロ値はnilではない
type Person struct {
    Name string
    Age  int
}

var p Person // ゼロ値:{Name: "", Age: 0}
// p != nil // コンパイルエラー

// ポインタのゼロ値はnil
var ptr *Person // nil

nilの実用的なパターン

1. オプショナル引数

func processData(data []int, callback func(int)) {
    for _, item := range data {
        if callback != nil {
            callback(item)
        }
    }
}

2. 遅延初期化

type Cache struct {
    data map[string]interface{}
}

func (c *Cache) Get(key string) interface{} {
    if c.data == nil {
        c.data = make(map[string]interface{})
    }
    return c.data[key]
}

3. エラーハンドリング

func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("division by zero")
    }
    return a / b, nil // エラーがない場合はnilを返す
}

4. リソースのクリーンアップ

type Resource struct {
    file *os.File
}

func (r *Resource) Close() error {
    if r.file != nil {
        err := r.file.Close()
        r.file = nil // 重複クローズを防ぐ
        return err
    }
    return nil
}

nilのイディオム

Guard句でのnil チェック

func process(data *Data) error {
    if data == nil {
        return errors.New("data cannot be nil")
    }
    // 処理続行
    return nil
}

nil interface{}の判定

func isEmpty(v interface{}) bool {
    return v == nil || reflect.ValueOf(v).IsNil()
}

nil チャネルの活用

// selectでnilチャネルを無効化
func worker(stop <-chan struct{}) {
    var input <-chan Work
    
    for {
        select {
        case work := <-input:
            // inputがnilなら永続的にブロック(この分岐は実行されない)
            processWork(work)
        case <-stop:
            input = nil // チャネルを無効化
            return
        }
    }
}

注意点

  1. typed nil: インターフェースに代入されたnilポインタはnilと等価比較できない
  2. nilポインタの逆参照: panic を引き起こす
  3. nilマップへの書き込み: panic を引き起こす
  4. nilチャネルの操作: 永続的にブロック

nilは Goにおける重要な概念で、適切に理解して使用することで、安全で効率的なコードを書くことができます。

カテゴリ:Go
カテゴリ:Go