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

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

このコミットは、Go言語の初期段階において、container/vector パッケージにイテレータ機能を追加したものです。具体的には、for ... range ループで使用できるチャネルベースのイテレータを導入し、vector の要素を効率的に走査できるようにしました。

コミット

commit 8d44052b6dd9e27a5230f66239681cec601d3a8e
Author: Rob Pike <r@golang.org>
Date:   Mon Mar 23 17:46:59 2009 -0700

    iterator for vector
    
    R=rsc
    DELTA=35  (35 added, 0 deleted, 0 changed)
    OCL=26662
    CL=26662
---
 src/lib/container/vector.go      | 16 ++++++++++++++++
 src/lib/container/vector_test.go | 19 +++++++++++++++++++
 2 files changed, 35 insertions(+)

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

https://github.com/golang/go/commit/8d44052b6dd9e27a5230f66239681cec601d3a8e

元コミット内容

iterator for vector

R=rsc
DELTA=35  (35 added, 0 deleted, 0 changed)
OCL=26662
CL=26662

変更の背景

このコミットは、Go言語がまだ活発に開発されていた初期の段階で行われました。当時のGoには、現在のような組み込みのイテレータプロトコルやジェネリクスは存在しませんでした。しかし、コレクション(この場合は vector)の要素を効率的かつ慣用的に走査するメカニズムは不可欠でした。

Go言語の設計思想の一つに「並行性 (concurrency) は言語の中心的な要素であるべき」というものがあります。この思想に基づき、Goはゴルーチン(軽量スレッド)とチャネル(ゴルーチン間の通信手段)を言語のプリミティブとして提供しています。このコミットは、このチャネルの特性を活かして、コレクションのイテレーションを実現しようとした初期の試みの一つです。

for ... range ループは、Go言語の強力な機能の一つであり、スライス、配列、マップ、文字列、そしてチャネルといった様々なデータ構造を簡潔に反復処理するために使用されます。このコミットの目的は、vector 型もこの for ... range ループの恩恵を受けられるようにすることでした。チャネルを介して要素をストリームとして提供することで、vector の内部実装を隠蔽しつつ、統一されたイテレーションインターフェースを提供することが可能になります。

前提知識の解説

Go言語のチャネル (Channels)

Go言語のチャネルは、ゴルーチン間で値を送受信するためのパイプのようなものです。チャネルは、並行処理における同期と通信の主要なメカニズムとして機能します。

  • 作成: c := make(chan Type) で作成します。Type はチャネルが送受信するデータの型です。
  • 送信: c <- value でチャネルに値を送信します。
  • 受信: value := <-c でチャネルから値を受信します。
  • クローズ: close(c) でチャネルをクローズします。クローズされたチャネルからの受信は、チャネルにまだ値が残っていればその値を返し、値がなくなればゼロ値を返します。クローズ後にチャネルに送信しようとするとパニックが発生します。
  • for ... range とチャネル: for v := range c のようにチャネルを for ... range ループで使用すると、チャネルがクローズされるまでチャネルから値を受信し続けます。チャネルがクローズされるとループは終了します。

ゴルーチン (Goroutines)

ゴルーチンは、Goランタイムによって管理される軽量な実行スレッドです。数千、数万のゴルーチンを同時に実行してもオーバーヘッドが非常に小さいのが特徴です。go キーワードの後に任意の関数呼び出しを記述することで、新しいゴルーチンを起動できます。

container/vector パッケージ (初期のGo)

Go言語の初期には、標準ライブラリに container パッケージが存在し、その中に vector 型が含まれていました。これは、動的な配列(現在のスライスに近い概念)を提供するものでした。現在のGoでは、組み込みのスライス型が非常に強力であるため、container/vector パッケージはほとんど使われなくなっています。しかし、このコミットが行われた当時は、vector がコレクションの基本的な実装の一つでした。

技術的詳細

このコミットは、vector 型に iterateIter という2つのメソッドを追加することで、チャネルベースのイテレータを実装しています。

  1. func (p *Vector) iterate(c chan Element):

    • このメソッドは、vector の内部配列 p.a をループで走査します。
    • 各要素を引数として受け取ったチャネル cc <- p.a[i] の形式で送信します。
    • 全ての要素を送信し終えた後、close(c) を呼び出してチャネルをクローズします。これは、for ... range ループがチャネルの終了を検知するために不可欠です。
    • このメソッドは、通常、別のゴルーチンで実行されることを意図しています。
  2. func (p *Vector) Iter() chan Element:

    • このメソッドは、イテレータの「ファクトリ」として機能します。
    • まず、c := make(chan Element) を使って新しいチャネルを作成します。このチャネルが、イテレーション中に要素をストリームとして提供するために使用されます。
    • 次に、go p.iterate(c) を呼び出して、iterate メソッドを新しいゴルーチンで起動します。これにより、iterate メソッドがバックグラウンドで要素をチャネルに送信し続けることができます。
    • 最後に、作成したチャネル c を返します。

この設計により、ユーザーは vector オブジェクトに対して Iter() メソッドを呼び出すことでチャネルを取得し、そのチャネルを for ... range ループで直接使用できるようになります。

// 例: vectorの要素をイテレートする
myVector := vector.NewIntVector(5)
myVector.Set(0, 10)
myVector.Set(1, 20)
// ...

for v := range myVector.Iter() {
    // v は vector の各要素
    fmt.Println(v)
}

このパターンは、Go言語における「ジェネレータ」または「コルーチン」のような動作を実現する一般的な方法です。チャネルがバッファリングされていない場合、iterate ゴルーチンは Iter() を呼び出したゴルーチンがチャネルから値を受信するまでブロックされます。これにより、要素がオンデマンドで生成されるかのように振る舞い、メモリ効率の良いイテレーションが可能になります。

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

src/lib/container/vector.go

 // 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]
 	}
 	close(c);
 }
 
 // Channel iterator for range.
 func (p *Vector) Iter() chan Element {
 	c := make(chan Element);
 	go p.iterate(c);
 	return c;
 }

src/lib/container/vector_test.go

 func TestIter(t *testing.T) {
 	const Len = 100;
 	x := vector.New(Len);
 	for i := 0; i < Len; i++ {
 		x.Set(i, i*i);
 	}
 	i := 0;
 	for v := range x.Iter() {
 		if v.(int) != i*i {
 			t.Error("Iter expected", i*i, "got", v.(int))
 		}
 		i++;
 	}
 	if i != Len {
 		t.Error("Iter stopped at", i, "not", Len)
 	}
 }

コアとなるコードの解説

src/lib/container/vector.go の変更

  • iterate メソッド:

    • c chan Element を引数に取り、Element 型の値を送信するためのチャネルを受け取ります。
    • for i := 0; i < len(p.a); i++ ループで vector の内部スライス p.a の全要素を走査します。
    • c <- p.a[i] で現在の要素をチャネル c に送信します。
    • ループが終了した後、close(c) を呼び出してチャネルをクローズします。これは、Iter() メソッドから返されたチャネルを for ... range ループで消費する際に、ループがいつ終了するかを通知するために非常に重要です。チャネルがクローズされないと、for ... range ループは永遠にブロックされる可能性があります。
  • Iter メソッド:

    • chan Element を戻り値として、要素をストリームとして提供するチャネルを返します。
    • c := make(chan Element) で新しいチャネルを作成します。このチャネルは、iterate ゴルーチンと Iter を呼び出したゴルーチン間の通信に使用されます。
    • go p.iterate(c) は、iterate メソッドを新しいゴルーチンとして起動します。これにより、iterate は非同期に要素をチャネルにプッシュし、Iter を呼び出した側はチャネルから要素をプルできるようになります。
    • return c で、作成したチャネルを呼び出し元に返します。このチャネルが for ... range ループの対象となります。

src/lib/container/vector_test.go の変更

  • TestIter 関数:
    • const Len = 100 でテスト用のベクターの長さを定義します。
    • x := vector.New(Len) で新しい vector インスタンスを作成します。
    • for i := 0; i < Len; i++ { x.Set(i, i*i); } で、ベクターに i*i の値を設定し、テストデータを準備します。
    • for v := range x.Iter() { ... } がこのテストの核心部分です。x.Iter() が返すチャネルを for ... range ループで消費し、ベクターの要素をイテレートします。
    • if v.(int) != i*i { t.Error(...) } で、イテレートされた値が期待通りであるか(i*i と一致するか)をアサートします。v.(int) は型アサーションで、Element インターフェースから具体的な int 型に変換しています。
    • i++ でイテレーションの回数をカウントします。
    • ループ終了後、if i != Len { t.Error(...) } で、全ての要素がイテレートされたか(iLen と一致するか)を確認します。

このテストは、Iter メソッドが正しくチャネルを介して vector の全要素を提供し、for ... range ループが期待通りに動作することを確認しています。

関連リンク

参考にした情報源リンク