[インデックス 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
:append
、copy
、typecheckaste
、typecheckas
、checkassignto
関数内での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
は非エクスポートフィールド(state
や sema
など)を含むため、そのコピーはコンパイラエラー (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/
) に集中しています。
-
doc/go_spec.html
:- 非エクスポートフィールドを持つ構造体の代入に関する制限を記述した段落が削除されました。
-
src/cmd/gc/go.h
:exportassignok
関数の宣言が削除されました。
-
src/cmd/gc/subr.c
:assignconv
関数内でのexportassignok
の呼び出しが削除されました。
-
src/cmd/gc/typecheck.c
:exportassignok
関数の定義全体が削除されました。append
、copy
、typecheckaste
、typecheckas
、checkassignto
関数内でのexportassignok
の呼び出しがすべて削除されました。
-
src/pkg/os/file_plan9.go
,src/pkg/os/file_unix.go
,src/pkg/os/file_windows.go
:os.File
構造体が、実際のファイル情報を保持する非エクスポートの*file
ポインタを匿名フィールドとして持つように変更されました。file
という新しい非エクスポート構造体が定義され、実際のfd
、name
、dirinfo
フィールドを持つようになりました。NewFile
関数およびOpenFile
関数が、File
構造体を作成する際に、内部でfile
構造体のインスタンスを生成し、そのポインタをFile
構造体に埋め込むように変更されました。- ファイナライザの設定が、
File
構造体から内部のfile
構造体に対して行われるように変更されました。 Close
メソッドが、内部のfile
構造体のclose
メソッドを呼び出すように変更されました。
-
src/pkg/sync/mutex.go
:- コメント
// Values containing the types defined in this package should not be copied.
が追加されました。これは、sync.Mutex
のような型はコピーすべきではないという慣習を明示するためのものです。
- コメント
-
test/assign.go
:sync.Mutex
やそれを含む構造体の代入に関するテストケースで、以前はコンパイラエラーを期待していた箇所が、エラーなし(// ok
)を期待するように変更されました。
-
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
インスタンスは元のインスタンスとは独立した fd
や name
のコピーを持つことになります。もし、元の 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
の変更は、非エクスポートフィールドを含む構造体のコピーが許可されるようになった新しいセマンティクスにおいて、重要な内部状態を持つ型が安全に扱われるための模範的なパターンを示しています。
関連リンク
- golang-dev メーリングリストでの議論 (2011年3月): https://groups.google.com/group/golang-dev/t/3f5d30938c7c45ef
- Go Code Review (CL) ページ: https://golang.org/cl/5372095
参考にした情報源リンク
- Go言語の公式ドキュメントおよびソースコード
- Go言語の非エクスポートフィールドと構造体のコピーに関する一般的な解説記事 (Web検索結果より)
- Goにおける構造体のコピーと非エクスポートフィールドの扱いに関する一般的な情報源は、Goの公式ドキュメントやブログ記事、コミュニティの議論に多く見られます。特に、
Clone()
メソッドの慣習やencoding/gob
、reflect
パッケージの使用例は、Goの構造体コピー戦略を理解する上で役立ちます。- Goにおける構造体のコピーと履歴管理に関する一般的な情報 (Web検索結果)
- Goの公式ブログやGoDocの
sync.Mutex
のドキュメントは、コピーに関する警告について言及しています。 - Go言語仕様の過去のバージョンと現在のバージョンを比較することで、この変更の具体的な影響をより深く理解できます。
- Go Programming Language Specification (現在の仕様)
- Go Programming Language Specification (過去のバージョン) (コミットで変更されたファイル)
- Goにおける構造体のコピーと非エクスポートフィールドの扱いに関する一般的な情報源は、Goの公式ドキュメントやブログ記事、コミュニティの議論に多く見られます。特に、