[インデックス 1422] ファイルの概要
このコミットは、Go言語の初期段階におけるチュートリアルプログラムの修正と、それらをテストスイートに組み込むことを目的としています。具体的には、Go言語のI/O操作におけるバイトスライスの扱いや、チャネルの初期化方法の変更に対応し、チュートリアルプログラムが正しく動作するように更新されています。また、これらのプログラムが継続的に動作することを保証するため、自動テストの一部として実行されるように設定が追加されています。
コミット
commit 8d21004b418e180f821e9d836c0bdaec262ecd21
Author: Rob Pike <r@golang.org>
Date: Tue Jan 6 15:49:27 2009 -0800
make the tutorial programs run again.
(the text still needs fixing)
add the tutorial programs to the test run.
R=rsc
DELTA=41 (6 added, 0 deleted, 35 changed)
OCL=22174
CL=22174
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/8d21004b418e180f821e9d836c0bdaec262ecd21
元コミット内容
make the tutorial programs run again.
(the text still needs fixing)
add the tutorial programs to the test run.
変更の背景
このコミットが行われた2009年1月は、Go言語がまだ一般に公開される前の非常に初期の段階でした。Go言語は活発に開発されており、言語仕様や標準ライブラリのAPIが頻繁に変更されていました。このような状況下では、既存のコード、特にチュートリアルやサンプルプログラムは、言語の進化に追従できなくなり、動作しなくなることがよくありました。
このコミットの主な背景は以下の2点です。
- 言語仕様・APIの変更への対応: Go言語のI/Oインターフェース(
Read、Writeメソッド)におけるバイトスライスの扱いや、チャネルの初期化方法(newとmakeの使い分け)が変更されたため、古いコードがコンパイルエラーになったり、正しく動作しなくなったりしていました。チュートリアルプログラムは、Go言語の基本的な概念を学ぶための重要なリソースであるため、これらが動作しないことは学習体験を著しく損ないます。 - テストカバレッジの拡大: チュートリアルプログラムが動作しなくなる問題を再発させないため、そしてGo言語の安定性を高めるために、これらのプログラムを自動テストスイートに組み込む必要がありました。これにより、将来の言語変更があった際にも、チュートリアルプログラムの動作が保証されるようになります。
コミットメッセージにある「(the text still needs fixing)」という記述は、プログラムコードは修正されたものの、それに対応するチュートリアル文書自体の更新はまだ残っていることを示唆しており、当時の開発の迅速さと、コードの修正が先行していた状況を反映しています。
前提知識の解説
このコミットを理解するためには、Go言語の初期の設計思想と、以下の基本的な概念についての知識が必要です。
1. Go言語のI/Oインターフェースとバイトスライス
Go言語では、データの読み書きにio.Readerとio.Writerというインターフェースが広く使われます。これらのインターフェースのReadおよびWriteメソッドは、バイトスライス([]byte)を引数として受け取ります。
Read(p []byte) (n int, err error):pにデータを読み込み、読み込んだバイト数nとエラーerrを返します。Write(p []byte) (n int, err error):pのデータを書き込み、書き込んだバイト数nとエラーerrを返します。
初期のGo言語では、これらのメソッドがポインタ型のバイトスライス(*[]byte)を引数として受け取っていた時期がありました。しかし、Goの設計思想として、スライス自体が参照型であるため、スライスへのポインタを渡す必要がないという判断がなされ、最終的には[]byteを直接渡す形に統一されました。これは、コードの簡潔さと、ポインタの二重間接参照を避けるための変更です。
2. Go言語のチャネルとnew vs make
Go言語のチャネル(chan)は、ゴルーチン間で値を安全に送受信するための強力な同期プリミティブです。チャネルを使用する前に、初期化する必要があります。
make関数:makeは、スライス、マップ、チャネルといった組み込みの参照型を初期化するために使用されます。makeは、これらの型の内部データ構造を適切に割り当て、初期化します。- 例:
ch := make(chan int)は、int型の値を送受信するためのチャネルを作成します。
- 例:
new関数:newは、任意の型のゼロ値へのポインタを返します。これはメモリを割り当てるだけで、その型のゼロ値を指すポインタを返します。チャネルの場合、new(chan int)はnilチャネルへのポインタを返しますが、これはそのままでは使用できません。チャネルはmakeで初期化されて初めて、送受信操作が可能になります。
初期のGo言語では、チャネルの初期化にnewが使われていた時期があったようですが、これは後にmakeに統一されました。makeを使用することで、チャネルが適切に初期化され、すぐに使用できる状態になります。
3. Go言語のテストフレームワークとシェルスクリプト
Go言語には、標準でtestingパッケージというテストフレームワークが組み込まれています。しかし、このコミットの時点では、Go言語のビルドシステムやテスト実行の仕組みはまだ発展途上でした。そのため、src/run.bashのようなシェルスクリプトが、コンパイル、テストの実行、結果の検証といった一連のプロセスを自動化するために用いられていました。これは、Go言語の初期開発における柔軟性と、既存のUnixツールとの親和性を示すものです。
技術的詳細
このコミットにおける技術的な変更は、主に以下の2つのパターンに分類されます。
1. バイトスライスのポインタから値渡しへの変更
doc/progs/cat.go, doc/progs/cat_rot13.go, doc/progs/fd.go, doc/progs/helloworld3.go の各ファイルで、ReadおよびWriteメソッドの引数として渡されるバイトスライスの型が *[]byte から []byte に変更されています。
- 変更前:
fd.Read(&buf)やFD.Stdout.Write(&hello)のように、バイトスライスのアドレスを渡していました。また、インターフェースの定義もRead(b *[]byte)のようになっていました。 - 変更後:
fd.Read(buf)やFD.Stdout.Write(hello)のように、バイトスライスを直接渡す形に変更されました。インターフェースの定義もRead(b []byte)に変更されています。
この変更は、Go言語におけるスライスのセマンティクスが固まる過程で生じたものです。スライスは内部的にポインタ、長さ、容量を持つ構造体であり、関数にスライスを渡すと、その構造体のコピーが渡されます。しかし、そのコピーされた構造体内のポインタは元の配列を指しているため、関数内でスライスの要素を変更すると、元のスライスにも変更が反映されます。したがって、スライス自体を変更(例えば、スライスの長さを変更したり、別の配列を指すようにしたり)する必要がない限り、スライスへのポインタを渡す必要はありません。この変更は、GoのI/OインターフェースをよりGoらしい(idiomatic)記述に統一するための重要なステップでした。
2. チャネルの初期化におけるnewからmakeへの変更
doc/progs/server.go, doc/progs/server1.go, doc/progs/sieve.go, doc/progs/sieve1.go の各ファイルで、チャネルの初期化方法が new(chan Type) から make(chan Type) に変更されています。
- 変更前:
ch := new(chan int)のように、new関数を使用してチャネルのポインタを取得していました。しかし、newはメモリを割り当てるだけで、チャネルを実際に使用可能な状態にはしません。 - 変更後:
ch := make(chan int)のように、make関数を使用してチャネルを初期化しています。makeはチャネルの内部構造を適切に初期化し、すぐに送受信操作ができるようにします。
この変更は、Go言語におけるnewとmakeの役割の明確化と、チャネルの正しい使用方法を確立するためのものです。newは主に構造体などのゼロ値へのポインタを取得するために使われ、スライス、マップ、チャネルといった組み込みの参照型はmakeで初期化するというGoの慣習が、この時期に確立されていきました。
3. チュートリアルプログラムのテストスイートへの追加
src/run.bash ファイルが変更され、doc/progs ディレクトリ内のチュートリアルプログラムがGo言語のメインテストスイートの一部として実行されるようになりました。
- 変更前:
doc/progs/runスクリプトは、個別に実行されることを想定していました。 - 変更後:
src/run.bashに(xcd ../doc/progs; time run) || exit $?という行が追加されました。これにより、src/run.bashが実行されるたびに、doc/progsディレクトリに移動し、その中のrunスクリプトが実行されるようになります。timeコマンドは実行時間を計測し、|| exit $?はrunスクリプトが失敗した場合に全体のテストを中断させるためのものです。
この変更は、Go言語の継続的インテグレーション(CI)の初期段階における重要な一歩であり、チュートリアルプログラムの品質と動作保証を自動化するためのものです。
コアとなるコードの変更箇所
doc/progs/cat.go (抜粋)
--- a/doc/progs/cat.go
+++ b/doc/progs/cat.go
@@ -13,14 +13,14 @@ func cat(fd *FD.FD) {
const NBUF = 512;
var buf [NBUF]byte;
for {
- switch nr, er := fd.Read(&buf); true {
+ switch nr, er := fd.Read(buf); true {
case nr < 0:
print("error reading from ", fd.Name(), ": ", er, "\n");
sys.exit(1);
case nr == 0: // EOF
return;
case nr > 0:
- if nw, ew := FD.Stdout.Write((&buf)[0:nr]); nw != nr {
+ if nw, ew := FD.Stdout.Write(buf[0:nr]); nw != nr {
print("error writing from ", fd.Name(), ": ", ew, "\n");
}
}
fd.Read(&buf) が fd.Read(buf) に、FD.Stdout.Write((&buf)[0:nr]) が FD.Stdout.Write(buf[0:nr]) に変更されています。これはバイトスライスのポインタ渡しから値渡しへの変更を示しています。
doc/progs/server1.go (抜粋)
--- a/doc/progs/server1.go
+++ b/doc/progs/server1.go
@@ -27,9 +27,9 @@ func Server(op *BinOp, service *chan *Request, quit *chan bool) {
}
}
-func StartServer(op *BinOp) (service *chan *Request, quit *chan bool) {
- service = new(chan *Request);
- quit = new(chan bool);
+func StartServer(op *BinOp) (service chan *Request, quit chan bool) {
+ service = make(chan *Request);
+ quit = make(chan bool);
go Server(op, service, quit);
return service, quit;
}
@@ -42,7 +42,7 @@ func main() {
req := &reqs[i];
req.a = i;
req.b = i + N;
- req.replyc = new(chan int);
+ req.replyc = make(chan int);
adder <- req;
}
for i := N-1; i >= 0; i-- { // doesn't matter what order
new(chan Type) が make(chan Type) に変更されています。これはチャネルの初期化方法の変更を示しています。また、関数の戻り値の型も *chan Type から chan Type に変更されています。
src/run.bash (抜粋)
--- a/src/run.bash
+++ b/src/run.bash
@@ -58,6 +58,10 @@ make smoketest
# # make test
# ) || exit $?\n
+(xcd ../doc/progs
+time run
+) || exit $?\n
+\n
(xcd ../test
./run
) || exit $?\n
doc/progs ディレクトリ内の run スクリプトを実行する行が追加されています。
コアとなるコードの解説
バイトスライスの変更 (*[]byte -> []byte)
Go言語において、スライスは配列へのポインタ、長さ、容量を持つ構造体です。関数にスライスを渡す際、この構造体自体は値渡しされますが、構造体内のポインタは元の配列を指しています。そのため、関数内でスライスの要素を変更すると、呼び出し元のスライスにもその変更が反映されます。
例えば、func Read(b []byte) の場合、b はスライスヘッダのコピーです。Read関数内で b[i] = someByte のように要素を書き換えると、元の配列のデータが変更されます。これは、C言語で配列をポインタとして渡すのと似た効果を持ちます。
初期のGo言語では、*[]byte のようにスライスへのポインタを渡す設計が試みられた時期もありましたが、これは冗長であり、Goのスライスのセマンティクスと合致しないと判断されました。スライス自体を再割り当てしたり、長さを変更したりするような操作(例えば b = append(b, ...))を関数内で行い、その変更を呼び出し元に反映させたい場合は、戻り値として新しいスライスを返すか、明示的にスライスへのポインタを渡す必要があります。しかし、ReadやWriteのように、既存のスライスバッファにデータを読み書きする操作では、スライスへのポインタは不要であり、[]byte を直接渡すのがより自然で効率的です。
この変更は、Go言語のAPI設計が成熟し、より簡潔で直感的なインターフェースが採用されていった過程を示しています。
チャネルの初期化 (new -> make)
new(chan int) は、chan int 型のゼロ値(nilチャネル)へのポインタを返します。nilチャネルは、送受信操作を行うとデッドロックを引き起こすか、ブロックし続けるため、実用的なチャネルとしては機能しません。
一方、make(chan int) は、チャネルの内部データ構造を適切に割り当て、初期化します。これにより、チャネルはすぐに送受信操作が可能になります。
Go言語では、スライス、マップ、チャネルといった組み込みの参照型は、その性質上、内部的なデータ構造の割り当てと初期化が必要です。make関数は、これらの型のために特別に設計されており、必要なメモリを割り当て、その型のゼロ値ではない、使用可能な状態に初期化します。
この変更は、Go言語の初期段階でnewとmakeの使い分けに関する混乱があったことを示唆しており、最終的にmakeがこれらの参照型の初期化に推奨される標準的な方法として確立されたことを反映しています。
テストスイートへの組み込み
src/run.bash への変更は、Go言語のテストインフラストラクチャの進化を示しています。チュートリアルプログラムのような重要なサンプルコードが、言語の変更によって動作しなくなることは、開発者にとって大きな問題です。これらを自動テストスイートに組み込むことで、以下のメリットが得られます。
- 回帰テスト: 将来の言語変更やライブラリの更新があった際に、チュートリアルプログラムが引き続き正しく動作するかどうかを自動的に検証できます。
- 品質保証: チュートリアルプログラムが常に最新の言語仕様とAPIに準拠していることを保証し、学習者が古い情報に基づいて誤ったコードを書くことを防ぎます。
- 開発効率: 開発者が手動で各チュートリアルプログラムを実行して動作確認する手間を省き、開発プロセスを効率化します。
このアプローチは、現代のソフトウェア開発における継続的インテグレーション(CI)の基本的な考え方と一致しており、Go言語の初期から品質と安定性を重視していたことが伺えます。
関連リンク
- Go言語の公式ドキュメント: https://go.dev/doc/
- Go言語のブログ(初期の言語変更に関する情報が見つかる可能性があります): https://go.dev/blog/
- Go言語のソースコードリポジトリ(コミット履歴をさらに深く掘り下げることができます): https://github.com/golang/go
参考にした情報源リンク
- Go言語の
makeとnewの違いに関する解説: - Go言語のスライスに関する解説:
- Go言語のI/Oインターフェースに関する解説:
(注: 上記の参考リンクは現在のGo言語のドキュメントですが、このコミットが行われた時期のGo言語の設計思想やAPIの変遷を理解する上で役立つ概念が含まれています。)