Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 10390] ファイルの概要

コミット

コミットハッシュ: a7f1e10d24ea36771c7f146bcf042b6ee32bfbcd
作成者: Russ Cox rsc@golang.org
日付: 2011年11月14日 16:10:58 (EST)
タイトル: fmt: distinguish empty vs nil slice/map in %#v

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/a7f1e10d24ea36771c7f146bcf042b6ee32bfbcd

元コミット内容

fmt: distinguish empty vs nil slice/map in %#v

Also update Scanf tests to cope with DeepEqual
distinguishing empty vs nil slice.

R=golang-dev, bradfitz
CC=golang-dev
https://golang.org/cl/5375091

このコミットは、Go言語のfmtパッケージにおいて、%#vフォーマット指定子でnilスライス・マップと空のスライス・マップを区別できるようにする重要な変更を行った。また、reflect.DeepEqualがnilスライスと空スライスを区別することに対応するため、Scanfテストの更新も併せて実施された。

変更の背景

問題の発生背景

Go言語の初期開発段階である2011年時点では、fmtパッケージの%#vフォーマット指定子は、nilスライス・マップと空のスライス・マップを同じように表示していた。この問題は、デバッグや開発時において以下の問題を引き起こしていた:

  1. デバッグ時の混乱: 開発者がnilスライス・マップなのか空のスライス・マップなのかを視覚的に区別できない
  2. テストの不整合: reflect.DeepEqualはnilと空のスライス・マップを異なるものとして扱うため、テストで予期しない結果が発生
  3. 型の意味論的差異の不明確さ: Goにおけるnilと空の値の概念的差異が表現できない

Goにおけるnilと空のスライス・マップの重要性

Go言語において、nilスライス・マップと空のスライス・マップは概念的に異なる意味を持つ:

  • nilスライス: var slice []int → 未初期化状態、メモリ確保なし
  • 空スライス: slice := []int{} → 初期化済み、空のデータ構造

これらの差異は、APIデザインやJSONマーシャリング、メモリ効率などの観点で重要な意味を持つ。

前提知識の解説

%#vフォーマット指定子

%#vは、Goの値をGo言語の構文で表現する特別なフォーマット指定子である。これにより、開発者は値がどのようにGoのコードとして表現されるかを確認できる。

// 改善前の動作例
var nilSlice []int
emptySlice := []int{}

fmt.Printf("%#v\n", nilSlice)   // []int{} (区別されない)
fmt.Printf("%#v\n", emptySlice) // []int{} (区別されない)

reflect.DeepEqualの動作

reflect.DeepEqualは、Go言語における深い等価性比較を行う関数である。この関数は、nilスライス・マップと空のスライス・マップを異なるものとして扱う:

var nilSlice []int
emptySlice := []int{}

reflect.DeepEqual(nilSlice, emptySlice) // false

この動作により、テストにおいて予期しない失敗が発生することがあった。

Go言語の型システムにおけるnilの概念

Go言語において、nilは以下の型の零値として定義されている:

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

スライスとマップの場合、nilは「存在しない」または「未初期化」の状態を表し、空のスライス・マップは「空である」状態を表す。

技術的詳細

実装アプローチ

このコミットでは、fmtパッケージのprint.goファイル内で、%#vフォーマット処理時にnilチェックを追加する実装を採用した。

マップ型の処理改善

case reflect.Map:
    if goSyntax {
        p.buf.WriteString(f.Type().String())
        if f.IsNil() {
            p.buf.WriteString("(nil)")
            break
        }
        p.buf.WriteByte('{')
    }

スライス型の処理改善

if goSyntax {
    p.buf.WriteString(value.Type().String())
    if f.IsNil() {
        p.buf.WriteString("(nil)")
        break
    }
    p.buf.WriteByte('{')
}

テストケースの追加

新しい動作を検証するため、以下のテストケースが追加された:

{"%#v", []int(nil), `[]int(nil)`},
{"%#v", []int{}, `[]int{}`},
{"%#v", map[int]byte(nil), `map[int] uint8(nil)`},
{"%#v", map[int]byte{}, `map[int] uint8{}`},

Scanfテストの更新

reflect.DeepEqualの動作変更に対応するため、テストでnilスライスを空スライスに変更:

// 変更前
{"", "", nil, nil, ""},

// 変更後  
{"", "", []interface{}{}, []interface{}{}, ""},

また、テストエラーメッセージもより詳細な%#vフォーマットを使用するように更新された。

コアとなるコードの変更箇所

1. print.go - マップ型の処理 (src/pkg/fmt/print.go:795-800)

case reflect.Map:
    if goSyntax {
        p.buf.WriteString(f.Type().String())
+       if f.IsNil() {
+           p.buf.WriteString("(nil)")
+           break
+       }
        p.buf.WriteByte('{')

2. print.go - スライス型の処理 (src/pkg/fmt/print.go:873-878)

if goSyntax {
    p.buf.WriteString(value.Type().String())
+   if f.IsNil() {
+       p.buf.WriteString("(nil)")
+       break
+   }
    p.buf.WriteByte('{')

3. fmt_test.go - テストケース追加 (src/pkg/fmt/fmt_test.go:357-361)

+ {"%#v", []int(nil), `[]int(nil)`},
+ {"%#v", []int{}, `[]int{}`},
+ {"%#v", map[int]byte(nil), `map[int] uint8(nil)`},
+ {"%#v", map[int]byte{}, `map[int] uint8{}`},

コアとなるコードの解説

nilチェックの実装詳細

追加されたnilチェックはreflect.Value.IsNil()メソッドを使用している。このメソッドは、値がnilポインタ、nil関数、nilインターフェース、nilスライス、nilマップ、nilチャンネルの場合にtrueを返す。

if f.IsNil() {
    p.buf.WriteString("(nil)")
    break
}

break文により、nilの場合は後続の処理(要素の出力など)をスキップし、効率的に処理を完了する。

型文字列の出力

f.Type().String()は、値の型を文字列として取得する。これにより、以下のような出力が可能になる:

  • []int(nil) - nilスライス
  • []int{} - 空スライス
  • map[int]uint8(nil) - nilマップ
  • map[int]uint8{} - 空マップ

テストの更新理由

Scanfテストの更新は、reflect.DeepEqualの動作に起因している。この関数は以下の規則でスライスを比較する:

  1. 両方ともnilか、両方ともnon-nilである必要がある
  2. 同じ長さである必要がある
  3. 対応する要素が深く等しい必要がある

nilスライスと空スライスは条件1を満たさないため、reflect.DeepEqualfalseを返す。

関連リンク

参考にした情報源リンク