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

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

このコミットは、Go言語の標準ライブラリであるcontainer/vectorパッケージ内のイテレータの実装を改善するものです。具体的には、Vector型のiterateメソッドにおいて、従来のインデックスベースのforループをGoのrangeキーワードを用いたイテレーションに置き換えることで、コードの可読性とGoらしいイディオムへの準拠を高めています。

コミット

commit 62d11a33024273073d00adcb7c0b9a62422d9d06
Author: Rob Pike <r@golang.org>
Date:   Wed Apr 1 16:34:25 2009 -0700

    use range in vector iterator
    
    R=rsc
    DELTA=2  (0 added, 0 deleted, 2 changed)
    OCL=27003
    CL=27003

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

https://github.com/golang/go/commit/62d11a33024273073d00adcb7c0b9a62422d9d06

元コミット内容

use range in vector iterator

変更の背景

このコミットは、Go言語がまだ初期開発段階にあった2009年に行われたものです。当時のGo言語は、言語仕様や標準ライブラリの設計が活発に進められていました。container/vectorパッケージは、動的配列(ベクター)のようなデータ構造を提供するもので、その要素をイテレートする機能が必要とされていました。

初期の実装では、C言語やJavaのような他の言語で一般的な、インデックス変数iを使って配列の長さを上限にループを回す形式が用いられていました。しかし、Go言語にはスライスやマップ、チャネルなどのコレクションを簡潔にイテレートするためのrangeキーワードが導入されていました。このrangeキーワードは、インデックスと値の両方、または値のみを効率的かつ安全に取得できるため、Goらしいイディオムとして推奨されていました。

この変更の背景には、Go言語の設計思想である「シンプルさ」「可読性」「イディオムの統一」があります。rangeキーワードを使用することで、イテレーションの意図がより明確になり、コードが簡潔になります。また、インデックスの範囲チェックを明示的に書く必要がなくなるため、バグの発生リスクを低減できるという利点もあります。このコミットは、Go言語の標準ライブラリが、その言語の持つ強力な機能を積極的に採用し、よりGoらしいコードベースへと進化していく過程の一部を示しています。

前提知識の解説

  1. Go言語のforループとrangeキーワード: Go言語には、他の多くの言語とは異なる独自のforループの構文があります。

    • 従来のforループ: for initialization; condition; post {} の形式で、C言語のforループに似ています。例えば、for i := 0; i < len(s); i++ のように使われます。これは、インデックスを使ってコレクションの要素にアクセスする場合に用いられます。
    • rangeキーワード: スライス、配列、文字列、マップ、チャネルなどのコレクションをイテレートするためのGo言語特有の構文です。for index, value := range collection {} の形式で、イテレーションごとにインデックスと値(またはどちらか一方)を返します。rangeは、コレクションの要素を安全かつ効率的に走査するための推奨される方法です。このコミットでは、rangeがスライス(p.aは内部的にスライス)に対してどのように機能するかが重要です。
  2. Go言語のcontainer/vectorパッケージ(初期のGoにおけるデータ構造): Go言語の初期には、containerパッケージ群(list, heap, vectorなど)が標準ライブラリとして提供されていました。container/vectorは、動的なサイズ変更が可能な要素のシーケンス(ベクター)を実装するためのパッケージでした。これは、現在のGoでスライスが提供する機能の一部を補完するものでした。

    • Vector型は、内部的にスライス(p.a)を使用して要素を保持していました。
    • iterateメソッドは、Vectorの要素を外部に提供するためのイテレータパターンを実装していました。このメソッドはチャネル(c chan Element)を使用して要素を送信し、呼び出し元がfor rangeでチャネルから要素を受け取れるように設計されていました。
  3. チャネル (Channels): Go言語の並行処理のプリミティブであり、ゴルーチン間で値を安全に送受信するためのパイプのようなものです。このコミットのiterateメソッドでは、c chan Elementというチャネルを引数に取り、p.aの各要素をこのチャネルに送信しています。イテレーションが完了すると、close(c)を呼び出してチャネルを閉じ、これ以上値が送信されないことを受信側に通知します。

技術的詳細

このコミットの技術的な核心は、Go言語のイテレーションのベストプラクティスへの移行です。

変更前は、iterateメソッドは以下のように実装されていました。

func (p *Vector) iterate(c chan Element) {
	for i := 0; i < len(p.a); i++ {
		c <- p.a[i]
	}
	close(c);
}

このコードは、p.aという内部スライスの長さをlen(p.a)で取得し、インデックスiを0からlen(p.a) - 1まで増やしながらループを回しています。ループの各イテレーションで、p.a[i]を使ってスライスの要素にアクセスし、その要素をチャネルcに送信しています。これは機能的には正しいですが、Go言語のイディオムとしては冗長であり、特にインデックス自体が必要ない場合には不必要に複雑です。

変更後は、rangeキーワードが導入されました。

func (p *Vector) iterate(c chan Element) {
	for i, v := range p.a {
		c <- v
	}
	close(c);
}

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

  1. 可読性の向上: for i, v := range p.aという構文は、「p.aの各要素について、そのインデックスをiに、値をvに割り当ててループを回す」という意図を明確に示します。これにより、コードの目的が一目で理解しやすくなります。
  2. 簡潔さ: インデックスiの初期化、条件式、インクリメントといった冗長な部分が不要になります。コード行数自体は変わっていませんが、表現がより洗練されています。
  3. Goらしいイディオムへの準拠: rangeはGo言語が提供する強力な機能であり、コレクションのイテレーションにおいて推奨される方法です。この変更は、標準ライブラリがGo言語の設計原則に沿って進化していることを示しています。
  4. 潜在的なバグの削減: インデックスベースのループでは、len(p.a)の計算ミスや、ループ条件の誤り(例: i <= len(p.a))など、オフバイワンエラーが発生する可能性があります。rangeを使用することで、これらの一般的なエラーを回避できます。rangeは、イテレート対象のコレクションの現在の状態に基づいて安全にイテレーションを行います。
  5. 効率性(多くの場合): コンパイラはrangeループを最適化し、インデックスベースのループと同等か、場合によってはより効率的なコードを生成することができます。特に、値のみが必要な場合は、インデックスを無視するfor _, v := range p.aの形式を使うことで、さらに最適化の機会が生まれます。このコミットではインデックスiも取得していますが、使用していないため、コンパイラが最適化する可能性があります。

この特定のコミットでは、i(インデックス)が使用されていないため、厳密にはfor _, v := range p.aと書くことも可能でした。しかし、当時のGoの慣習や、将来的にインデックスが必要になる可能性を考慮してi, vの形式が選ばれたのかもしれません。重要なのは、インデックスベースのアクセスからrangeベースのイテレーションへの移行という本質的な変更です。

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

変更はsrc/lib/container/vector.goファイル内で行われています。

--- a/src/lib/container/vector.go
+++ b/src/lib/container/vector.go
@@ -214,8 +214,8 @@ func (p *Vector) Swap(i, j int) {
 
 // Iterate over all elements; driver for range
 func (p *Vector) iterate(c chan Element) {
-	for i := 0; i < len(p.a); i++ {
-		c <- p.a[i]
+	for i, v := range p.a {
+		c <- v
 	}
 	close(c);
 }

コアとなるコードの解説

変更されたのはVector型のiterateメソッドです。このメソッドは、Vectorの内部スライスp.aの要素を、引数として渡されたチャネルcに送信する役割を担っています。

  • 変更前:

    for i := 0; i < len(p.a); i++ {
    	c <- p.a[i]
    }
    

    この行では、iという整数型のインデックス変数を0からp.aの長さの直前まで増加させながらループを回していました。ループ本体では、p.a[i]を使ってインデックスiに対応する要素にアクセスし、その要素をチャネルcに送信していました。これは一般的な配列の走査方法ですが、Goにおいてはより簡潔な方法が存在します。

  • 変更後:

    for i, v := range p.a {
    	c <- v
    }
    

    この行では、Goのrangeキーワードが使用されています。range p.aは、p.aスライスの各要素について、そのインデックスと値をペアで返します。このペアはそれぞれivに代入されます。ループ本体では、v(要素の値)を直接チャネルcに送信しています。インデックスiは取得されていますが、この特定のコンテキストでは使用されていません。この変更により、コードはよりGoらしく、意図が明確になりました。

close(c)は、イテレーションが完了した後にチャネルを閉じることで、受信側がこれ以上値が来ないことを検知できるようにするための重要な処理であり、変更前後で共通しています。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント (Go Language Specification)
  • Go言語のソースコード (GitHubリポジトリ)
  • Go言語に関する一般的なプログラミング知識