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

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

このコミットは、Go言語の初期開発段階における重要な変更点を示しています。具体的には、スライス(slice)の型表現を *[]T から []T へと変更するものです。これは、Go言語におけるスライスのセマンティクスを、より直感的で効率的なものにするための根本的な設計変更でした。

コミット

commit d47d888ba663014e6aa8ca043e694f4b2a5898b8
Author: Russ Cox <rsc@golang.org>
Date:   Thu Dec 18 22:37:22 2008 -0800

    convert *[] to [].

    R=r
    OCL=21563
    CL=21571

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

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

元コミット内容

convert *[] to [].

この簡潔なコミットメッセージは、Go言語のスライス型が *[]T から []T へと変更されたことを示しています。これは、スライスがポインタ型として扱われていた初期の設計から、現在の値型として扱われる設計への移行を意味します。

変更の背景

Go言語の初期の設計では、スライスは内部的に配列へのポインタとして実装されており、その型は *[]T のように表現されていました。これは、スライスが参照型であることを明示する意図があったと考えられます。しかし、この表現は、スライスが配列とは異なる独自のセマンティクスを持つこと、特にスライスヘッダ自体が値として扱われることと矛盾が生じました。

スライスは、基盤となる配列へのポインタ、長さ(len)、および容量(cap)という3つの要素からなる構造体です。*[]T という表記は、この構造体へのポインタを意味し、スライスを関数に渡す際に & 演算子を使ってアドレスを渡す必要がありました。これは冗長であり、スライスの直感的な利用を妨げる可能性がありました。

このコミットは、スライスをより自然な方法で扱えるようにするための言語設計の改善の一環です。[]T という表記にすることで、スライスがGo言語の他の組み込み型(例:int, string)と同様に、値として扱われることを明確にしました。これにより、スライスを関数に渡す際に明示的なポインタ渡しが不要になり、コードの可読性と記述性が向上しました。

前提知識の解説

この変更を理解するためには、以下の概念が重要です。

  • ポインタ (Pointers): メモリ上の特定のアドレスを指し示す変数です。Go言語では * 記号を使ってポインタ型を宣言し、& 演算子で変数のアドレスを取得します。
  • 配列 (Arrays): 同じ型の要素が連続してメモリに配置された固定長のデータ構造です。Go言語の配列は値型であり、関数に渡すとコピーされます。
  • スライス (Slices): Go言語における動的な配列のようなものです。スライスは、基盤となる配列の一部を参照します。スライス自体は、基盤配列へのポインタ、長さ、容量の3つの要素を持つ構造体です。
  • 値型と参照型:
    • 値型: 変数に直接値が格納されます。関数に渡されると、その値のコピーが作成されます。Go言語の int, string, struct (ポインタを含まない場合) などがこれに該当します。
    • 参照型: 変数には値そのものではなく、値が格納されているメモリ上のアドレス(参照)が格納されます。関数に渡されると、参照のコピーが作成されますが、参照が指す先のデータは共有されます。Go言語の map, channel, function などがこれに該当します。スライスも、その内部構造(基盤配列へのポインタ)から参照型のように振る舞いますが、スライスヘッダ自体は値型です。

このコミットの変更は、スライスヘッダを値型として扱うというGo言語の設計思想をより明確に反映したものです。

技術的詳細

このコミットの核心は、Go言語におけるスライスの型定義と、それに伴うコンパイラおよびランタイムの挙動の変更です。

変更前 (*[]T) のスライス:

  • スライスは「T 型の要素を持つ配列へのポインタ」として概念化されていました。
  • 型宣言は *[]T の形式でした。例えば、バイトスライスは *[]byte と書かれました。
  • 関数にスライスを渡す場合、スライスヘッダ(基盤配列へのポインタ、長さ、容量)のアドレスを渡す必要がありました。これは func foo(s *[]byte) のように定義され、呼び出し側では foo(&mySlice) のように & 演算子が必要でした。
  • nil スライスは、nil ポインタとして表現されました。

変更後 ([]T) のスライス:

  • スライスは「T 型の要素を持つスライスヘッダ」として概念化されました。このスライスヘッダは、基盤配列へのポインタ、長さ、容量を含む構造体です。
  • 型宣言は []T の形式になりました。例えば、バイトスライスは []byte と書かれます。
  • 関数にスライスを渡す場合、スライスヘッダの値がコピーされます。これにより、呼び出し側で & 演算子を使う必要がなくなりました。func foo(s []byte) のように定義され、呼び出し側では foo(mySlice) のように直接スライス変数を渡します。
  • スライスヘッダがコピーされるため、関数内でスライスの長さや容量を変更しても、呼び出し元のスライス変数には反映されません(ただし、スライスの要素自体を変更した場合は、基盤配列が共有されているため、呼び出し元にも反映されます)。スライスの長さや容量の変更を呼び出し元に反映させるには、変更されたスライスを戻り値として返す必要があります。
  • nil スライスは、スライスヘッダのすべての要素がゼロ値(ポインタが nil、長さと容量が 0)である状態として表現されます。

この変更により、Go言語のスライスは、C++の参照やJavaの配列参照とは異なる、Go独自のセマンティクスを持つことになりました。スライスは「値型」として扱われるが、その値は「参照」を含むという、一見すると矛盾するような特性を持っています。しかし、この設計は、スライスの操作をより安全かつ直感的に行えるようにするためのものです。例えば、スライスを関数に渡しても、そのスライスヘッダがコピーされるため、関数内でスライス変数を別のスライスに再割り当てしても、呼び出し元のスライスには影響しません。

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

このコミットでは、Go言語の標準ライブラリの非常に多くのファイルで *[]T から []T への変更が行われています。以下に、その代表的な変更箇所をいくつか示します。

src/cmd/gc/sys.go:

--- a/src/cmd/gc/sys.go
+++ b/src/cmd/gc/sys.go
@@ -95,7 +95,7 @@ export func	stringtorune(string, int) (int, int);	// convert bytes to runes
 
 export func	exit(int);
 
-export func	symdat() (symtab *[]byte, pclntab *[]byte);
+export func	symdat() (symtab []byte, pclntab []byte);
 
 export func	semacquire(sema *int32);
 export func	semrelease(sema *int32);

symdat 関数の戻り値の型が *[]byte から []byte に変更されています。

src/lib/bufio.go:

--- a/src/lib/bufio.go
+++ b/src/lib/bufio.go
@@ -30,7 +30,7 @@ export var (
 	ShortWrite = os.NewError("short write");
 )
 
-func CopySlice(dst *[]byte, src *[]byte) {
+func CopySlice(dst []byte, src []byte) {
 	for i := 0; i < len(dst); i++ {
 		dst[i] = src[i]
 	}
@@ -40,7 +40,7 @@ func CopySlice(dst *[]byte, src *[]byte) {
 // Buffered input.
 
 export type BufRead struct {
-	buf *[]byte;
+	buf []byte;
 	rd io.Read;
 	r, w int;
 	err *os.Error;
@@ -91,7 +91,7 @@ func (b *BufRead) Fill() *os.Error {
 // Returns the number of bytes read into p.
 // If nn < len(p), also returns an error explaining
 // why the read is short.
-func (b *BufRead) Read(p *[]byte) (nn int, err *os.Error) {
+func (b *BufRead) Read(p []byte) (nn int, err *os.Error) {
 	nn = 0;
 	for len(p) > 0 {
 		n := len(p);

CopySlice 関数の引数、BufRead 構造体のフィールド、Read メソッドの引数など、バイトスライスを扱う多くの箇所で *[]byte[]byte に変更されています。

src/lib/net/ip.go:

--- a/src/lib/net/ip.go
+++ b/src/lib/net/ip.go
@@ -22,7 +22,7 @@ export const (
 )
 
 // Make the 4 bytes into an IPv4 address (in IPv6 form)
-func MakeIPv4(a, b, c, d byte) *[]byte {
+func MakeIPv4(a, b, c, d byte) []byte {
 	p := new([]byte, IPv6len);
 	for i := 0; i < 10; i++ {
 		p[i] = 0
@@ -37,7 +37,9 @@ func MakeIPv4(a, b, c, d byte) *[]byte {
 }
 
 // Well-known IP addresses
-export var IPv4bcast, IPv4allsys, IPv4allrouter, IPv4prefix, IPallbits, IPnoaddr *[]byte
+export var IPv4bcast, IPv4allsys, IPv4allrouter, IPv4prefix, IPallbits, IPnoaddr []byte
+\n+var NIL []byte // TODO(rsc)
 
 func init() {
 	IPv4bcast = MakeIPv4(0xff, 0xff, 0xff, 0xff);

IPアドレスを表すバイトスライスも *[]byte から []byte に変更されています。また、nil スライスを表現するための NIL []byte 変数が導入されている点も注目に値します。これは、nil ポインタとしての nil と、nil スライスとしての nil の区別を明確にするための過渡期の措置と考えられます。

コアとなるコードの解説

この変更は、Go言語のコンパイラとランタイムがスライスをどのように扱うかという、根本的な部分に影響を与えます。

  1. 型システムの一貫性: []T という表記は、スライスがGo言語の他の組み込み型と同様に、値として扱われることを強調します。これにより、型システム全体の一貫性が向上します。
  2. 関数呼び出しの簡素化: *[]T の場合、関数にスライスを渡す際には常に & 演算子が必要でした。[]T に変更されたことで、スライスを直接渡すことができるようになり、コードがより簡潔になりました。
    • 変更前: func process(data *[]byte) { ... }; process(&myBytes)
    • 変更後: func process(data []byte) { ... }; process(myBytes)
  3. スライスヘッダのコピー: []T はスライスヘッダ(ポインタ、長さ、容量)の値を表します。関数に渡されると、このスライスヘッダがコピーされます。これにより、関数内でスライスの長さや容量を変更しても、呼び出し元のスライス変数には影響しません。これは、スライスが「値型」として振る舞うことの重要な側面です。
    func modifySlice(s []int) {
        s = append(s, 4) // s は新しいスライスヘッダを指すようになる
        s[0] = 99       // 基盤配列の要素は変更される
    }
    
    func main() {
        mySlice := []int{1, 2, 3}
        modifySlice(mySlice)
        fmt.Println(mySlice) // 出力: [99 2 3] (長さは変わらない)
    }
    
    もし modifySlice*[]int を受け取っていた場合、s = append(s, 4)*s = append(*s, 4) となり、呼び出し元の mySlice も新しいスライスヘッダを指すように変更される可能性がありました。[]T にすることで、この挙動がより明確になります。
  4. nil スライスの扱い: *[]T の時代には、nil スライスは nil ポインタでした。[]T になったことで、nil スライスは、基盤配列へのポインタが nil で、長さと容量が 0 のスライスヘッダを持つものとして扱われます。これは、len(nilSlice)0 を返し、cap(nilSlice)0 を返すという現在のGoの挙動に繋がります。

この変更は、Go言語のスライスが持つ「参照型のような振る舞いをする値型」というユニークな特性を確立する上で不可欠なステップでした。これにより、スライスは強力かつ安全なデータ構造として、Goプログラミングにおいて中心的な役割を果たすようになりました。

関連リンク

  • Go言語の公式ドキュメント: https://go.dev/doc/
  • Go Slices: usage and internals: https://go.dev/blog/slices-intro (このブログ記事は、スライスの内部構造と使用法について詳しく解説しており、このコミットの背景にある設計思想を理解するのに役立ちます。)

参考にした情報源リンク

  • Go言語のソースコード (GitHub): https://github.com/golang/go
  • Go言語の初期のコミット履歴
  • Go言語に関する各種技術ブログやフォーラムでの議論 (特にスライスのセマンティクスに関するもの)
  • Go言語の仕様書 (Go Language Specification)
  • Go Slices: usage and internals - The Go Programming Language: https://go.dev/blog/slices-intro