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

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

このコミットは、Go言語において、非エクスポート(unexported)フィールドを含む構造体(struct)のコピーを許可するように変更するものです。これにより、パッケージが reflect.Value のような「不透明な値(opaque values)」をAPIとして提供する際に、よりクリーンな方法で実現できるようになります。これまでは、非エクスポートフィールドを含む構造体のコピーは、その構造体が定義されているパッケージ内でのみ許可されていました。この変更は、Goの型システムにおける重要なセマンティクスの変更であり、コンパイラ (gc) の内部ロジック、Go言語仕様のドキュメント、および標準ライブラリの一部 (os.File) に影響を与えています。

コミット

  • コミットハッシュ: d03611f628c65321b759fc6
  • Author: Russ Cox rsc@golang.org
  • Date: Tue Nov 15 12:20:59 2011 -0500
  • Subject: allow copy of struct containing unexported fields

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

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

元コミット内容

commit d03611f628c65321b572ab0d4ce85cc61b759fc6
Author: Russ Cox <rsc@golang.org>
Date:   Tue Nov 15 12:20:59 2011 -0500

    allow copy of struct containing unexported fields

    An experiment: allow structs to be copied even if they
    contain unexported fields.  This gives packages the
    ability to return opaque values in their APIs, like reflect
    does for reflect.Value but without the kludgy hacks reflect
    resorts to.

    In general, we trust programmers not to do silly things
    like *x = *y on a package's struct pointers, just as we trust
    programmers not to do unicode.Letter = unicode.Digit,
    but packages that want a harder guarantee can introduce
    an extra level of indirection, like in the changes to os.File
    in this CL or by using an interface type.

    All in one CL so that it can be rolled back more easily if
    we decide this is a bad idea.

    Originally discussed in March 2011.
    https://groups.google.com/group/golang-dev/t/3f5d30938c7c45ef

    R=golang-dev, adg, dvyukov, r, bradfitz, jan.mercl, gri
    CC=golang-dev
    https://golang.org/cl/5372095

変更の背景

この変更の主な背景は、Go言語のパッケージ設計における「不透明な値(opaque values)」の扱いです。Goでは、構造体のフィールドが非エクスポート(小文字で始まる)である場合、そのフィールドは定義されたパッケージ内からのみアクセス可能です。これまでのGoの仕様では、非エクスポートフィールドを含む構造体は、その構造体が定義されているパッケージの外部ではコピー(代入や関数引数としての受け渡しなど)が許可されていませんでした。これは、外部パッケージが内部実装の詳細に意図せず依存したり、変更したりすることを防ぐための設計上の制約でした。

しかし、この制約は reflect.Value のような特定のAPI設計において不便をもたらしていました。reflect.Value は、Goの型システムを動的に操作するための重要な型ですが、その内部には非エクスポートフィールドが含まれています。このため、reflect.Value をコピーする際には、Goのコンパイラが特別に許可する「kludgy hacks(ごまかしのハック)」に頼る必要がありました。これは、言語の整合性を損なう可能性があり、よりクリーンな解決策が求められていました。

このコミットは、非エクスポートフィールドを含む構造体のコピーを一般的に許可することで、この問題を解決しようとする「実験的な試み」です。Russ Coxは、プログラマが *x = *y のような操作をパッケージの構造体ポインタに対して「愚かなこと」をしないと信頼していると述べています。しかし、より厳密な保証が必要なパッケージのために、os.File の変更のように、追加の間接参照レイヤーを導入するか、インターフェース型を使用するという代替手段も示唆しています。

この変更は、Goの型システムにおける根本的な変更であるため、問題が発生した場合に容易にロールバックできるよう、関連するすべての変更が1つのコミットにまとめられています。この議論は2011年3月に golang-dev メーリングリストで開始されました。

前提知識の解説

Goにおけるエクスポートされたフィールドと非エクスポートされたフィールド

Go言語では、識別子(変数名、関数名、型名、構造体フィールド名など)の最初の文字が大文字であるか小文字であるかによって、その可視性(visibility)が決定されます。

  • エクスポートされた識別子(Exported identifiers): 最初の文字が大文字で始まる識別子は、その識別子が定義されているパッケージの外部からもアクセス可能です。これは、他のパッケージから利用できるAPIの一部となります。
  • 非エクスポートされた識別子(Unexported identifiers): 最初の文字が小文字で始まる識別子は、その識別子が定義されているパッケージ内からのみアクセス可能です。これは、パッケージの内部実装の詳細であり、外部からは直接利用できません。

構造体のフィールドもこのルールに従います。非エクスポートフィールドは、パッケージの内部状態をカプセル化し、外部からの不適切な変更を防ぐために使用されます。

Goの構造体(struct)のコピーセマンティクス

Goでは、構造体は値型(value type)です。これは、構造体を代入したり、関数に引数として渡したりする際に、その構造体の値全体がコピーされることを意味します。例えば、var a MyStruct; var b MyStruct = a のように代入すると、a のすべてのフィールドの値が b にコピーされます。

これまでのGoの仕様では、非エクスポートフィールドを含む構造体の場合、この値のコピーが、その構造体が定義されているパッケージの外部では許可されていませんでした。これは、外部パッケージが内部状態を直接コピーすることで、カプセル化が破られることを防ぐためです。

reflect パッケージの役割と「不透明な値」

reflect パッケージは、Goプログラムが実行時に自身の構造を検査し、操作するための機能を提供します。これにより、型情報、フィールド、メソッドなどを動的に取得したり、値を設定したりすることが可能になります。

reflect.Value は、任意のGoの値を抽象的に表現する型です。この型は、その内部に元の値へのポインタや型情報など、非エクスポートフィールドとして保持しています。reflect.Value のような型は、その内部構造を外部に公開せず、特定のAPIを通じてのみ操作を許可する「不透明な値」の典型例です。

これまでのGoでは、reflect.Value のような非エクスポートフィールドを含む構造体を外部パッケージでコピーしようとすると、コンパイラエラーが発生しました。しかし、reflect パッケージ自体はGoの標準ライブラリの一部であり、その機能は不可欠です。そのため、コンパイラは reflect.Value のコピーを特別に許可するような「ごまかしのハック」を内部的に持っていました。このコミットは、そのような特殊な扱いを一般化し、より一貫性のある言語セマンティクスを提供しようとするものです。

Goの型システムとパッケージ境界を越えたアクセス制限

Goの型システムは、パッケージをモジュール化の単位として重視しています。パッケージは、関連する機能とデータをカプセル化し、明確なAPI(エクスポートされた識別子)を通じてのみ外部とやり取りすることを推奨しています。非エクスポートフィールドの制限は、このカプセル化の原則を強制する重要なメカニズムの一つです。

sync.Mutex のような、コピーが禁止されている型について

sync.Mutex は、Goの並行処理において排他制御を行うためのミューテックス(相互排他ロック)です。sync.Mutex は、その内部状態(ロックの状態など)が非常に重要であり、コピーされると予期せぬ動作やデッドロックを引き起こす可能性があります。そのため、sync.Mutex のような型は、Goの慣習として「コピーしてはいけない」とされています。

このコミットの変更により、非エクスポートフィールドを含む構造体のコピーが一般的に許可されるようになりますが、これは sync.Mutex のような型をコピーしても安全になるという意味ではありません。むしろ、sync.Mutex のような型は、その性質上、コピーされるべきではないという原則は変わりません。このコミットは、プログラマがそのような型をコピーしないという「信頼」に基づいています。もし厳密なコピー禁止が必要な場合は、os.File の変更のように、ポインタやインターフェースを介した間接参照を導入することが推奨されます。

技術的詳細

このコミットの技術的詳細の核心は、Goコンパイラ (gc) が非エクスポートフィールドを含む構造体のコピーをどのように扱っていたかの変更と、それに伴う言語仕様の更新、そして標準ライブラリの調整です。

Goコンパイラ (gc) の変更

以前のGoコンパイラでは、構造体の代入やコピー操作が行われる際に、その構造体が非エクスポートフィールドを含んでいるかどうか、そしてその操作が構造体が定義されているパッケージ内で行われているかどうかをチェックするロジックが存在しました。このチェックは主に src/cmd/gc/typecheck.c 内の exportassignok 関数によって行われていました。

exportassignok 関数は、与えられた型 t が、暗黙的な代入(implicit assignment)によって外部パッケージからアクセスできない非エクスポートフィールドを含んでいないかを再帰的にチェックしていました。もしそのようなフィールドが見つかり、かつ代入が外部パッケージから行われている場合、コンパイラはエラーを報告していました。

このコミットでは、この exportassignok 関数が完全に削除されました。これにより、コンパイラは非エクスポートフィールドを含む構造体のコピーに対して特別な制限を課さなくなりました。これは、Goの型システムにおける根本的な変更であり、コンパイラが「非エクスポートフィールドを含む構造体は、パッケージ境界を越えても値としてコピー可能である」と解釈するようになったことを意味します。

具体的には、以下のファイルから exportassignok の呼び出しや関連するロジックが削除されています。

  • src/cmd/gc/go.h: exportassignok 関数の宣言が削除。
  • src/cmd/gc/subr.c: assignconv 関数内での exportassignok の呼び出しが削除。
  • src/cmd/gc/typecheck.c: appendcopytypecheckastetypecheckascheckassignto 関数内での exportassignok の呼び出し、および exportassignok 関数自体の定義が削除。

Go言語仕様 (doc/go_spec.html) の変更

Go言語仕様の doc/go_spec.html ファイルも更新され、非エクスポートフィールドを含む構造体の代入に関する以前の制限を記述した段落が削除されました。

削除された段落は以下の内容でした:

<p>
-If <code>T</code> is a struct type with non-<a href="#Exported_identifiers">exported</a>
-fields, the assignment must be in the same package in which <code>T</code> is declared,
-or <code>x</code> must be the receiver of a method call.
-In other words, a struct value can be assigned to a struct variable only if
-every field of the struct may be legally assigned individually by the program,
-or if the assignment is initializing the receiver of a method of the struct type.
-</p>

この削除により、言語仕様は非エクスポートフィールドを含む構造体のコピーに関する制限がなくなったことを反映しています。

os.File の変更(間接参照の追加)

このコミットは、os.File 構造体にも変更を加えています。これは、非エクスポートフィールドを含む構造体のコピーが許可されるようになったことによる潜在的な問題を緩和するためのものです。

os.File は、ファイルディスクリプタ(fd)やファイル名(name)などの重要な内部状態を持つ構造体です。これらのフィールドは非エクスポートされています。もし os.File の値が安易にコピーされ、そのコピーが変更された場合、元の os.File の状態と同期が取れなくなり、特にファイナライザ(runtime.SetFinalizer で設定される Close メソッド)が誤ったファイルディスクリプタを閉じてしまうなどの問題が発生する可能性があります。

この問題を回避するため、os.File は以下のように変更されました。

type File struct {
	*file // 匿名フィールドとして、内部の `file` 構造体へのポインタを持つ
}

// file is the real representation of *File.
// The extra level of indirection ensures that no clients of os
// can overwrite this data, which could cause the finalizer
// to close the wrong file descriptor.
type file struct {
	fd      int
	name    string
	dirinfo *dirInfo // nil unless directory being read
	// ...
}
  • File 構造体は、実際のファイル情報を保持する非エクスポートの file 構造体へのポインタ *file を匿名フィールドとして持つようになりました。
  • NewFile 関数や OpenFile 関数は、File 構造体を作成する際に、内部で file 構造体のインスタンスを生成し、そのポインタを File 構造体に埋め込むように変更されました。
  • ファイナライザは、File 構造体自体ではなく、内部の file 構造体に対して設定されるようになりました (runtime.SetFinalizer(f.file, (*file).close))。
  • Close メソッドは、File 構造体から内部の file 構造体の close メソッドを呼び出すように変更されました。

この変更により、os.File の値がコピーされたとしても、コピーされるのは *file ポインタの値であり、実際の file 構造体自体はコピーされません。これにより、複数の File インスタンスが同じ基盤となる file 構造体を共有し、内部状態の一貫性が保たれるようになります。これは、パッケージのクライアントが os.File の内部データを誤って上書きするのを防ぐための「追加の間接参照レイヤー」として機能します。

sync.Mutex のテストケースの変更

test/assign.go ファイルは、sync.Mutex の代入に関するテストケースを含んでいます。以前は、sync.Mutex は非エクスポートフィールド(statesema など)を含むため、そのコピーはコンパイラエラー (ERROR "assignment.*Mutex") となっていました。

このコミットの変更により、非エクスポートフィールドを含む構造体のコピーが許可されるようになったため、sync.Mutex のコピーもコンパイラエラーではなくなりました。したがって、test/assign.go 内の関連するテストケースは、エラーを期待する記述から「ok」を期待する記述に修正されました。

例:

--- a/test/assign.go
+++ b/test/assign.go
@@ -16,38 +16,38 @@ type T struct {
 func main() {
  	{
  		var x, y sync.Mutex
- 		x = y	// ERROR "assignment.*Mutex"
+ 		x = y // ok
  		_ = x
  	}
  	{
  		var x, y T
- 		x = y	// ERROR "assignment.*Mutex"
+ 		x = y // ok
  		_ = x
  	}
  	{
  		var x, y [2]sync.Mutex
- 		x = y	// ERROR "assignment.*Mutex"
+ 		x = y // ok
  		_ = x
  	}
  	{
  		var x, y [2]T
- 		x = y	// ERROR "assignment.*Mutex"
+ 		x = y // ok
  		_ = x
  	}
  	{
- 		x := sync.Mutex{0, 0}	// ERROR "assignment.*Mutex"
+ 		x := sync.Mutex{0, 0} // ERROR "assignment.*Mutex"
  		_ = x
  	}
  	{
- 		x := sync.Mutex{key: 0}	// ERROR "(unknown|assignment).*Mutex"
+ 		x := sync.Mutex{key: 0} // ERROR "(unknown|assignment).*Mutex"
  		_ = x
  	}
  	{
- 		x := &sync.Mutex{}	// ok
- 		var y sync.Mutex	// ok
- 		y = *x	// ERROR "assignment.*Mutex"
- 		*x = y	// ERROR "assignment.*Mutex"
+ 		x := &sync.Mutex{} // ok
+ 		var y sync.Mutex   // ok
+ 		y = *x             // ok
+ 		*x = y             // ok
  		_ = x
  		_ = y
- 	}
+ 	}
 }

この変更は、sync.Mutex をコピーしてもコンパイラエラーにはならないことを示していますが、前述の通り、sync.Mutex はコピーすべきではないという慣習は変わりません。これは、コンパイラがその制約を強制しなくなっただけであり、プログラマの責任で適切な使用法を守る必要があることを意味します。

削除されたバグテストファイル

このコミットでは、複数の test/fixedbugs/bugXXX.go ファイルが削除されています。これらのファイルは、非エクスポートフィールドを含む構造体のコピーが禁止されていたことに関連するコンパイラエラーをテストするためのものでした。このコミットによってその制限が解除されたため、これらのテストはもはや必要なくなり、削除されました。

削除されたファイル:

  • test/fixedbugs/bug226.dir/x.go
  • test/fixedbugs/bug226.dir/y.go
  • test/fixedbugs/bug226.go
  • test/fixedbugs/bug310.go
  • test/fixedbugs/bug359.go
  • test/fixedbugs/bug378.go

これらの削除は、Go言語のセマンティクスが変更され、以前はバグとして扱われていた挙動が、今では正当な挙動となったことを明確に示しています。

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

このコミットにおけるコアとなるコードの変更は、主にGoコンパイラ (src/cmd/gc/) とGo言語仕様のドキュメント (doc/go_spec.html)、そして os パッケージ (src/pkg/os/) に集中しています。

  1. doc/go_spec.html:

    • 非エクスポートフィールドを持つ構造体の代入に関する制限を記述した段落が削除されました。
  2. src/cmd/gc/go.h:

    • exportassignok 関数の宣言が削除されました。
  3. src/cmd/gc/subr.c:

    • assignconv 関数内での exportassignok の呼び出しが削除されました。
  4. src/cmd/gc/typecheck.c:

    • exportassignok 関数の定義全体が削除されました。
    • appendcopytypecheckastetypecheckascheckassignto 関数内での exportassignok の呼び出しがすべて削除されました。
  5. src/pkg/os/file_plan9.go, src/pkg/os/file_unix.go, src/pkg/os/file_windows.go:

    • os.File 構造体が、実際のファイル情報を保持する非エクスポートの *file ポインタを匿名フィールドとして持つように変更されました。
    • file という新しい非エクスポート構造体が定義され、実際の fdnamedirinfo フィールドを持つようになりました。
    • NewFile 関数および OpenFile 関数が、File 構造体を作成する際に、内部で file 構造体のインスタンスを生成し、そのポインタを File 構造体に埋め込むように変更されました。
    • ファイナライザの設定が、File 構造体から内部の file 構造体に対して行われるように変更されました。
    • Close メソッドが、内部の file 構造体の close メソッドを呼び出すように変更されました。
  6. src/pkg/sync/mutex.go:

    • コメント // Values containing the types defined in this package should not be copied. が追加されました。これは、sync.Mutex のような型はコピーすべきではないという慣習を明示するためのものです。
  7. test/assign.go:

    • sync.Mutex やそれを含む構造体の代入に関するテストケースで、以前はコンパイラエラーを期待していた箇所が、エラーなし(// ok)を期待するように変更されました。
  8. test/fixedbugs/ 以下の複数のファイル:

    • 非エクスポートフィールドを含む構造体のコピーが禁止されていたことに関連するバグテストファイルが削除されました。

コアとなるコードの解説

exportassignok 関数の削除とその影響

このコミットの最も重要な変更は、Goコンパイラ (gc) から exportassignok 関数が完全に削除されたことです。

以前の exportassignok の役割: exportassignok 関数は、Goの型チェックフェーズにおいて、ある型 t が、その型が定義されているパッケージの外部から暗黙的に代入される際に問題がないかを検証する役割を担っていました。具体的には、t が非エクスポートフィールドを含む構造体である場合、その代入が許可されないようにエラーを発生させていました。これは、パッケージのカプセル化を強制し、外部パッケージが内部実装の詳細にアクセスしたり、変更したりすることを防ぐための重要なメカニズムでした。

削除による影響: exportassignok が削除されたことにより、Goコンパイラはもはや非エクスポートフィールドを含む構造体のコピーに対して特別な制限を課さなくなりました。これは、Go言語のセマンティクスが変更され、非エクスポートフィールドを含む構造体も、他の値型と同様に、パッケージ境界を越えて値としてコピーできるようになることを意味します。

この変更は、reflect.Value のような「不透明な値」をAPIとして提供する際の「ごまかしのハック」を不要にするという目的を達成します。しかし、同時に、sync.Mutex のようにコピーされると問題を引き起こす可能性のある型についても、コンパイラがそのコピーを阻止しなくなるという副作用も持ちます。そのため、プログラマは、そのような型をコピーしないという慣習をより強く意識する必要があります。

os.File 構造体の変更

os.File 構造体の変更は、exportassignok の削除によって生じる可能性のある問題を緩和するための防御的なプログラミングの例です。

変更前: os.File は直接 fd (ファイルディスクリプタ) や name (ファイル名) といった非エクスポートフィールドを持っていました。

type File struct {
	fd      int
	name    string
	dirinfo *dirInfo // nil unless directory being read
}

この構造体が値としてコピーされると、コピーされた File インスタンスは元のインスタンスとは独立した fdname のコピーを持つことになります。もし、元の File インスタンスが閉じられたり、ファイナライザによって fd が無効化されたりしても、コピーされたインスタンスは古い fd を持ち続ける可能性があり、これは「use-after-free」のような問題や、誤ったリソースの解放につながる恐れがありました。

変更後: os.File は、実際のファイル情報を保持する非エクスポートの file 構造体へのポインタを匿名フィールドとして持つようになりました。

type File struct {
	*file // 匿名フィールドとして、内部の `file` 構造体へのポインタを持つ
}

type file struct { // 非エクスポート構造体
	fd      int
	name    string
	dirinfo *dirInfo
}

この変更により、File 構造体が値としてコピーされたとしても、コピーされるのは *file ポインタの値(つまり、file 構造体へのメモリアドレス)であり、実際の file 構造体自体はコピーされません。結果として、複数の File インスタンスが同じ基盤となる file 構造体を共有することになります。

利点:

  • 一貫性の維持: File のコピーが作成されても、すべてのコピーは同じ file 構造体を指すため、内部状態(fd など)は常に一貫しています。
  • ファイナライザの安全性: ファイナライザは file 構造体に対して設定されるため、File インスタンスがGCによって回収される際に、正しい file 構造体の close メソッドが呼び出され、ファイルディスクリプタの二重解放や解放漏れを防ぐことができます。
  • 不適切な上書きの防止: os パッケージのクライアントが File の値をコピーしてそのフィールドを直接変更しようとしても、それはポインタのコピーを変更するだけであり、元の file 構造体のデータには影響を与えません。これにより、パッケージの内部状態が外部から不適切に操作されることを防ぎます。

この os.File の変更は、非エクスポートフィールドを含む構造体のコピーが許可されるようになった新しいセマンティクスにおいて、重要な内部状態を持つ型が安全に扱われるための模範的なパターンを示しています。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメントおよびソースコード
  • Go言語の非エクスポートフィールドと構造体のコピーに関する一般的な解説記事 (Web検索結果より)
    • Goにおける構造体のコピーと非エクスポートフィールドの扱いに関する一般的な情報源は、Goの公式ドキュメントやブログ記事、コミュニティの議論に多く見られます。特に、Clone() メソッドの慣習や encoding/gobreflect パッケージの使用例は、Goの構造体コピー戦略を理解する上で役立ちます。