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

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

このコミットは、Go言語の標準ライブラリである container/heap パッケージの example_test.go ファイルに対する変更です。具体的には、ヒープの Push 操作の例を簡素化し、より堅牢にするための修正が行われています。

コミット

commit cfc0a59d6e4e11b08b7b7085e6da031643879b0a
Author: Frithjof Schulze <schulze@math.uni-hannover.de>
Date:   Tue Dec 4 14:11:33 2012 -0800

    container/heap: Simplify the example.
    
    Using append simplifies the code and makes it work if
    the initial capacity of the slice is smaller than the
    number of items pushed.
    
    R=golang-dev, gri
    CC=golang-dev
    https://golang.org/cl/6869060

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

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

元コミット内容

container/heap: Simplify the example.

Using append simplifies the code and makes it work if
the initial capacity of the slice is smaller than the
number of items pushed.

R=golang-dev, gri
CC=golang-dev
https://golang.org/cl/6869060

変更の背景

このコミットの背景には、Go言語のスライス(slice)の挙動、特にその容量(capacity)と append 関数の利用に関する理解の深化があります。

container/heap パッケージは、Go言語でヒープデータ構造を実装するための汎用的なインターフェースを提供します。このパッケージを利用する際には、ユーザーが heap.Interface を実装したカスタム型(通常はスライスを基盤とする)を定義し、その Push および Pop メソッド内でスライスの要素を追加・削除するロジックを記述する必要があります。

変更前の example_test.go 内の Push メソッドの実装では、新しい要素を追加するために、スライスの長さを手動で拡張し、その後に新しい要素を代入していました。具体的には、a = a[0 : n+1] のようにスライスを再スライスすることで長さを増やしていました。このアプローチの問題点は、もし基盤となる配列の容量が不足している場合、この操作だけでは新しい要素のためのメモリが確保されず、実行時パニック(panic)を引き起こす可能性があったことです。つまり、スライスの初期容量が Push されるアイテム数よりも小さい場合に、コードが正しく動作しないという潜在的なバグがありました。

このコミットは、この問題を解決し、コードをより簡潔にするために、Go言語の組み込み関数である append を使用するように Push メソッドのロジックを変更しています。append 関数は、必要に応じて基盤となる配列の容量を自動的に拡張する機能を持っているため、手動での容量管理や再スライスによる長さの拡張が不要になります。これにより、コードが簡素化されるだけでなく、スライスの初期容量に関わらず、Push 操作が常に正しく機能するようになります。

前提知識の解説

Goの container/heap パッケージについて

container/heap パッケージは、Go言語でヒープデータ構造を実装するための汎用的なインターフェースと関数を提供します。ヒープは、優先度キューなどの実装によく用いられるツリーベースのデータ構造で、親ノードの値が子ノードの値よりも常に大きい(または小さい)というヒーププロパティを満たします。

このパッケージを利用するには、以下の heap.Interface を実装した型を定義する必要があります。

type Interface interface {
    sort.Interface // Len, Less, Swap を含む
    Push(x any)
    Pop() any
}
  • Len() int: ヒープ内の要素数を返します。
  • Less(i, j int) bool: i 番目の要素が j 番目の要素よりも優先度が高い場合に true を返します(最小ヒープなら h[i] < h[j]、最大ヒープなら h[i] > h[j])。
  • Swap(i, j int): i 番目と j 番目の要素を入れ替えます。
  • Push(x any): ヒープに要素 x を追加します。
  • Pop() any: ヒープから最も優先度の高い要素を削除し、返します。

heap パッケージは、これらのインターフェースを実装した任意の型に対して、heap.Init(ヒープの初期化)、heap.Push(要素の追加)、heap.Pop(要素の削除)、heap.Remove(任意の要素の削除)、heap.Fix(要素の優先度変更後のヒープの再構築)といった操作を提供します。

Goの slice について

Go言語のスライスは、可変長シーケンスを表現するための強力なデータ型です。スライスは、基盤となる配列(underlying array)への参照、長さ(length)、そして容量(capacity)の3つの要素から構成されます。

  • 長さ (Length): スライスに含まれる要素の数です。len(s) で取得できます。
  • 容量 (Capacity): スライスの基盤となる配列のサイズです。スライスが拡張できる最大長を示します。cap(s) で取得できます。

スライスは、基盤となる配列の一部を「ビュー」として提供します。スライスを再スライス(s[low:high])することで、既存の基盤配列の異なる部分を参照したり、長さを変更したりできます。しかし、この操作は基盤配列の容量を超えることはできません。容量を超えて要素を追加しようとすると、パニックが発生します。

append 関数の挙動とスライス容量の自動拡張

Go言語の組み込み関数 append は、スライスに要素を追加するために使用されます。append の最も重要な特性は、追加操作によってスライスの長さが容量を超える場合、Goランタイムが自動的に新しい、より大きな基盤配列を割り当て、既存の要素を新しい配列にコピーし、その後に新しい要素を追加するという点です。

append 関数による容量の拡張ロジックは、Goのバージョンによって若干異なりますが、一般的には以下のようになります。

  • 小規模なスライス: 要素数が少ないスライスの場合、容量は通常2倍に拡張されます。これにより、頻繁な再割り当てを避け、パフォーマンスを向上させます。
  • 大規模なスライス: 要素数が多くなると、容量の増加率はより控えめになり、例えば1.25倍(25%増)など、より小さな係数で増加します。これは、大規模なスライスで過剰なメモリ割り当てを防ぐためです。

この自動的な容量拡張のメカニズムにより、開発者はスライスのメモリ管理について細かく心配する必要がなくなり、より簡潔で堅牢なコードを書くことができます。

技術的詳細

このコミットにおける技術的な変更の核心は、container/heap パッケージの example_test.go 内の PriorityQueue 型の Push メソッドの実装を、手動でのスライス長拡張から append 関数を使用する形に切り替えた点です。

変更前の Push メソッドは以下のロジックでした。

func (pq *PriorityQueue) Push(x interface{}) {
	// ...
	// To simplify indexing expressions in these methods, we save a copy of the
	// slice object. We could instead write (*pq)[i].
	a := *pq
	n := len(a)
	a = a[0 : n+1] // ここでスライスの長さを手動で拡張
	item := x.(*Item)
	item.index = n
	a[n] = item // 新しい要素を代入
	*pq = a
}

このコードでは、まず *pqa にコピーし、len(a) で現在の長さを取得します。次に、a = a[0 : n+1] という再スライス操作によって、スライスの長さを1増やしています。この操作は、基盤となる配列の容量が十分に大きい場合にのみ成功します。もし容量が不足している場合、この行で実行時パニックが発生します。その後、a[n] = item で新しい要素を代入し、最後に *pq = a で変更を元のポインタに反映しています。

変更後の Push メソッドは以下のようになります。

func (pq *PriorityQueue) Push(x interface{}) {
	// ...
	n := len(*pq)
	item := x.(*Item)
	item.index = n
	*pq = append(*pq, item) // append 関数を使用
}

この変更により、以下の点が改善されました。

  1. 簡潔性: a := *pqa = a[0 : n+1]*pq = a といった冗長なコードが不要になり、*pq = append(*pq, item) の1行で要素の追加が完結します。
  2. 堅牢性: append 関数は、必要に応じて基盤となる配列の容量を自動的に拡張するため、スライスの初期容量が不足している場合でもパニックを起こすことなく、正しく要素を追加できます。これにより、Push メソッドがより堅牢になります。
  3. 慣用的なGoコード: append 関数はGo言語でスライスに要素を追加する際の標準的かつ推奨される方法であり、この変更によってコードがよりGoらしい(idiomatic Go)記述になりました。

item.index = n の行は、ヒープ操作において要素のインデックスを追跡するために必要であり、Push 操作によって要素が追加される位置(新しい長さ n)を記録しています。この部分は append の使用とは直接関係なく、ヒープの内部管理のために残されています。

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

src/pkg/container/heap/example_test.go ファイルの Push メソッドが変更されました。

--- a/src/pkg/container/heap/example_test.go
+++ b/src/pkg/container/heap/example_test.go
@@ -37,15 +37,10 @@ func (pq PriorityQueue) Swap(i, j int) {
 func (pq *PriorityQueue) Push(x interface{}) {
 	// Push and Pop use pointer receivers because they modify the slice's length,
 	// not just its contents.
-	// To simplify indexing expressions in these methods, we save a copy of the
-	// slice object. We could instead write (*pq)[i].
-	a := *pq
-	n := len(a)
-	a = a[0 : n+1]
+	n := len(*pq)
 	item := x.(*Item)
 	item.index = n
-	a[n] = item
-	*pq = a
+	*pq = append(*pq, item)
 }
 
 func (pq *PriorityQueue) Pop() interface{} {

コアとなるコードの解説

変更された Push メソッドは、PriorityQueue 型(これは []*Item のスライス)に新しい Item を追加する役割を担います。

変更前:

func (pq *PriorityQueue) Push(x interface{}) {
	// ...
	a := *pq         // ポインタが指すスライスをコピー
	n := len(a)      // コピーしたスライスの現在の長さを取得
	a = a[0 : n+1]   // スライスの長さを1増やす(容量が足りない場合はパニック)
	item := x.(*Item)
	item.index = n   // 新しいアイテムのインデックスを設定
	a[n] = item      // 新しいアイテムをスライスの末尾に追加
	*pq = a          // 変更を元のスライスに反映
}

この旧来の実装では、スライスの長さを手動で a[0 : n+1] のように再スライスすることで拡張していました。この方法は、基盤となる配列の容量が不足している場合に問題を引き起こします。例えば、make(PriorityQueue, 0, 1) のように容量1で初期化されたスライスに2つ目の要素を Push しようとすると、a = a[0 : n+1] の行でパニックが発生します。

変更後:

func (pq *PriorityQueue) Push(x interface{}) {
	// ...
	n := len(*pq)      // 現在のスライスの長さを取得
	item := x.(*Item)
	item.index = n     // 新しいアイテムのインデックスを設定
	*pq = append(*pq, item) // append 関数を使ってアイテムを追加
}

新しい実装では、*pq = append(*pq, item) の1行で要素の追加が完結しています。append 関数は、Goランタイムがスライスの容量を自動的に管理することを可能にします。もし現在の容量が新しい要素を追加するのに十分でなければ、append は自動的に新しい、より大きな基盤配列を割り当て、既存の要素をコピーし、その後に新しい要素を追加します。これにより、開発者はスライスの容量不足によるパニックを心配する必要がなくなり、コードがより堅牢で簡潔になります。

item.index = n の行は、ヒープ内の要素の位置を追跡するために重要です。container/heap パッケージの FixRemove といった関数は、要素の現在のインデックスを知っている必要があります。Push される際に、その要素がスライスの末尾(現在の長さ n の位置)に追加されるため、そのインデックスを item.index に設定しています。

関連リンク

参考にした情報源リンク