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

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

コミット

  • コミットハッシュ: 9a7dd719448071e8e812deeb4757ebf2abff9cff
  • 作成者: Russ Cox rsc@golang.org
  • 作成日時: 2011年10月17日 14:51:45 -0400
  • コミットメッセージ: template: do not depend on map iteration order

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

https://github.com/golang/go/commit/9a7dd719448071e8e812deeb4757ebf2abff9cff

元コミット内容

このコミットは、Go言語の template パッケージに対する小さな修正を含んでいます:

  • 変更ファイル: src/pkg/template/exec_test.go
  • 変更内容: 1行の挿入、1行の削除
  • コードレビュー: https://golang.org/cl/5268048

変更の背景

2011年当時、Go言語は1.0リリースに向けて活発に開発されていました。この時期、Go開発チームは言語仕様の安定化と、将来の互換性を確保するための重要な決定を行っていました。

特に、マップの反復順序に関する問題は、Go 1.0で導入された重要な変更の一つです。Go 1.0以前では、マップの反復順序は実装依存で、開発者が特定の順序に依存したコードを書くことがありました。これは将来的な互換性の問題となる可能性があったため、Go 1.0では意図的にマップの反復順序をランダム化する決定がなされました。

このコミットは、そのような背景の中で、template パッケージのテストコードがマップの反復順序に依存しないようにするための修正です。

前提知識の解説

Go言語におけるマップの反復順序

Go言語では、マップ(map)の反復順序は仕様上保証されていません。Go 1.0以降、ランタイムは意図的にマップの反復順序をランダム化しています。

// マップの反復順序は毎回異なる可能性がある
m := map[string]int{"a": 1, "b": 2, "c": 3}
for k, v := range m {
    fmt.Printf("%s: %d\n", k, v)
}

テンプレートパッケージの特殊性

一方、text/template および html/template パッケージでは、マップの反復において特別な扱いがなされています:

  • 基本型のキーを持つマップの場合、キーがソートされた順序で反復される
  • これにより、テンプレートの出力が予測可能で再現可能になる

テストコードの重要性

Go言語では、テストコードが言語仕様の一部として機能することがあります。特に、標準ライブラリのテストコードは:

  1. 仕様の文書化: コードの期待される動作を示す
  2. 回帰テストの役割: 将来の変更による破壊的変更を検出
  3. 実装の検証: 仕様通りの動作を保証

技術的詳細

マップ反復順序の実装上の課題

Go 1.0以前では、マップの反復順序は実装詳細に依存していました。これは以下の問題を引き起こしていました:

  1. プラットフォーム依存: 異なるアーキテクチャで異なる順序
  2. バージョン依存: Go言語のバージョンによる順序の変化
  3. 実行時変動: 同一プログラム内でも順序が変わる可能性

ランダム化の実装

Go 1.0では、マップの反復順序をランダム化するために以下の仕組みが導入されました:

// 内部実装(概念的な例)
type hmap struct {
    B     uint8  // log_2 of # of buckets
    hash0 uint32 // hash seed
    // ... その他のフィールド
}

// 反復開始時にランダムな位置から開始
func mapiterinit(t *maptype, h *hmap, it *hiter) {
    r := uintptr(fastrand())
    // ランダムな位置から反復を開始
    it.startBucket = r & bucketMask(h.B)
    it.offset = uint8(r >> h.B & (bucketCnt - 1))
}

テンプレートパッケージでの対応

テンプレートパッケージでは、マップの反復順序を決定論的にするため、以下のような実装が行われています:

// 基本型のキーをソートする機能
func sortKeys(v []reflect.Value) []reflect.Value {
    if len(v) == 0 {
        return v
    }

    switch v[0].Kind() {
    case reflect.String:
        sort.Sort(rvStrings{v})
    case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
        sort.Sort(rvInts{v})
    case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
        sort.Sort(rvUints{v})
    case reflect.Float32, reflect.Float64:
        sort.Sort(rvFloats{v})
    }
    return v
}

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

コミットの詳細な変更内容:

  • ファイル: src/pkg/template/exec_test.go
  • 変更行数: 1行の挿入、1行の削除
  • 変更タイプ: テストコードの修正

このコミットは、テストコードがマップの反復順序に依存しないようにするための修正です。具体的には、テストの期待値や検証方法を、マップの反復順序に依存しない形に変更したと考えられます。

コアとなるコードの解説

テスト修正の意義

このコミットの変更は小さいものですが、以下の重要な意義があります:

  1. 将来の互換性確保: Go 1.0でのマップランダム化に備えた対応
  2. テストの堅牢性向上: 実装詳細に依存しないテストの作成
  3. 仕様の明確化: マップ反復順序に依存しない設計の推進

実装パターン

マップの反復順序に依存しないテストを書く際の一般的なパターン:

// 悪い例:順序に依存するテスト
func TestMapIteration(t *testing.T) {
    m := map[string]int{"a": 1, "b": 2, "c": 3}
    var keys []string
    for k := range m {
        keys = append(keys, k)
    }
    expected := []string{"a", "b", "c"}
    if !reflect.DeepEqual(keys, expected) {
        t.Errorf("Expected %v, got %v", expected, keys)
    }
}

// 良い例:順序に依存しないテスト
func TestMapIteration(t *testing.T) {
    m := map[string]int{"a": 1, "b": 2, "c": 3}
    var keys []string
    for k := range m {
        keys = append(keys, k)
    }
    sort.Strings(keys)
    expected := []string{"a", "b", "c"}
    if !reflect.DeepEqual(keys, expected) {
        t.Errorf("Expected %v, got %v", expected, keys)
    }
}

テンプレートパッケージでの実装

テンプレートパッケージでは、以下のような仕組みでマップの反復順序を制御しています:

// {{range}}アクションでマップを処理する際の実装
func (s *state) walk(dot reflect.Value, node parse.Node) {
    switch node := node.(type) {
    case *parse.RangeNode:
        s.walkRange(dot, node)
    }
}

func (s *state) walkRange(dot reflect.Value, r *parse.RangeNode) {
    val := s.evalPipeline(dot, r.Pipe)
    switch val.Kind() {
    case reflect.Map:
        s.walkMapRange(dot, r, val)
    }
}

func (s *state) walkMapRange(dot reflect.Value, r *parse.RangeNode, val reflect.Value) {
    keys := val.MapKeys()
    if len(keys) == 0 {
        return
    }

    // 基本型のキーの場合、ソート済みの順序で反復
    keys = sortKeys(keys)

    for _, key := range keys {
        elem := val.MapIndex(key)
        // テンプレートを実行
        s.setTopVar(1, key)
        s.setTopVar(2, elem)
        s.walk(dot, r.List)
    }
}

関連リンク

  1. Go言語仕様書 - マップ型
  2. text/template パッケージドキュメント
  3. Go 1.0リリースノート
  4. Go言語における決定論的動作の重要性

参考にした情報源リンク

  1. Go's map iteration order is random - Stack Overflow
  2. Go template package documentation
  3. Go map randomization discussion - Hacker News
  4. Go 1.0 development history
  5. Template map iteration order - Go Forum

このコミットは、Go言語の設計哲学である「明示的で予測可能な動作」を体現した重要な変更として、Go 1.0リリースに向けた準備作業の一環として位置づけられます。小さな変更ながら、Go言語の長期的な互換性と開発者体験の向上に貢献した意義深い修正です。