この記事では、Go言語のgoogle.golang.org/protobufパッケージを利用しているときに見かけたDoNotCopy
、DoNotCompare
、DoNotImplement
の3つのテクニックについて紹介します。
➡ DoNotCopy:構造体のシャローコピーを防止
➡ DoNotCompare:構造体の比較を防止
➡ DoNotImplement:インターフェースの実装を制限
➡ (おまけ)NoUnkeyedLiterals:構造体のインスタンス生成時にキー指定を強制
DoNotCopy
DoNotCopy
は以下のような実装となっています。
(protobuf-go/internal/pragma/pragma.go)
// DoNotCopy can be embedded in a struct to help prevent shallow copies.
// This does not rely on a Go language feature, but rather a special case
// within the vet checker.
//
// See https://golang.org/issues/8005.
type DoNotCopy [0]sync.Mutex
DoNotCopyのコメントには以下の通り記載されています。
DoNotCopy は、構造体に埋め込むことでシャローコピーの防止に役立てることができます。これは Go 言語の機能に依存するのではなく、vet チェッカー内の特別なケースによるものです。
つまり、DoNotCopy
はある構造体のシャローコピーを防ぐために利用するもののようです。
ただしこれはGoの仕様ではなくGo vetによるチェックに引っかかるだけなので、ビルドと実行は可能な点に注意が必要です。
次のようなコードで検証してみると、Go vetの警告が出ますがプログラムは実行できることが確認できます。
➡Try on Go Playground
type DoNotCopy [0]sync.Mutex
type Foo struct {
DoNotCopy
Bar string
}
func main() {
f := Foo{
Bar: "Hello!",
}
ff := f
fmt.Printf("%#v\n", ff)
}
# [play]
./prog.go:21:8: assignment copies lock value to ff: play.Foo contains sync.Mutex
./prog.go:22:22: call of fmt.Printf copies lock value: play.Foo contains sync.Mutex
Go vet failed.
main.Foo{DoNotCopy:main.DoNotCopy{}, Bar:"Hello!"}
Program exited.
また、このチェックはsync.Lockerのインターフェースを実装しているか否かが判定基準なので、次のような構造体を利用することでも可能となります。
このテクニックは、syncパッケージなどの標準パッケージ内でも利用されているようです。
type DoNotCopy struct{}
func (*DoNotCopy) Lock() {}
func (*DoNotCopy) Unlock() {}
DoNotCompare
DoNotCompare
は以下のように実装されています。
(protobuf-go/internal/pragma/pragma.go)
// DoNotCompare can be embedded in a struct to prevent comparability.
type DoNotCompare [0]func()
DoNotCompareのコメントには以下の通り記載されています。
DoNotCompare は、構造体に埋め込むことで比較を防ぐことができます。
つまり、DoNotCompareを利用すると構造体の比較を防ぐことができます。
以下のような構造体を比較するコードで検証してみると、実際にビルドに失敗することが確認できます。
➡ Try on Go Playground
type DoNotCompare [0]func()
type Foo struct {
DoNotCompare
Bar string
}
func main() {
f := Foo{
Bar: "Hello!",
}
ff := Foo{
Bar: "World!",
}
fmt.Println("f==ff", f == ff)
}
./prog.go:23:23: invalid operation: f == ff (struct containing DoNotCompare cannot be compared)
Go build failed.
これは、Goの言語仕様として下に挙げた2点があるためです(参考:spec#Comparison operators)。
Struct types are comparable if all their field types are comparable.
Slice, map, and function types are not comparable.
関数は比較不可であり、それを要素に持つスライスもまた比較不可となります。さらに、比較不可な型をフィールドに持つ構造体もまた比較不可となります。
DoNotCopyとは異なり、これは言語仕様に依存したテクニックであるためビルドにも失敗します。
なお、この言語仕様に基づくと以下のような定義でも同様の機能が実現できます。
ただし、見た目のシンプルさとメモリ効率を考慮するとtype DoNotCompare [0]func()
を利用するのが良いように思われます。
type DoNotCompare func()
type DoNotCompare [0]func()
type DoNotCompare map[string]struct{}
type DoNotCompare [0]map[string]struct{}
type DoNotCompare []struct{}
type DoNotCompare [0][]struct{}
DoNotImplement
DoNotImplement
は以下のよう実装されています。
(protobuf-go/internal/pragma/pragma.go)
// DoNotImplement can be embedded in an interface to prevent trivial
// implementations of the interface.
//
// This is useful to prevent unauthorized implementations of an interface
// so that it can be extended in the future for any protobuf language changes.
type DoNotImplement interface{ ProtoInternal(DoNotImplement) }
DoNotImplementのコメントには以下のように記載されています。
DoNotImplement は、インターフェースに埋め込むことで、そのインターフェースの安易な実装を防ぐことができます。これは、許可されていないインターフェースの実装を防ぐために役立ち、将来的な protobuf の言語変更にも対応できるようにするためのものです。
この説明だけでは具体的な使い方のイメージがつかないので、実際に利用されている箇所を確認してみます。
DoNotImplementは、Protobufのパッケージ内において、例えばFileImportsインターフェースなどで利用されています。
type doNotImplement pragma.DoNotImplement
type FileImports interface {
Len() int
Get(i int) FileImport
doNotImplement
}
では、「インターフェースの安易な実装」とはどういう意味か、上記のFileImports
インターフェースを実装する構造体を定義してみます。
次のような構造体Foo
を定義して、FileImportsインターフェースを実装することを試みましたが、この実装は機能しません。なぜならprotoreflect.DoNotImplement
インターフェースがinternalパッケージに定義されているからです。加えてDoNotImplement
インターフェースは自分自身を循環参照しているため、Foo構造体が知らない間にFileImports
インターフェースを実装していた!なんてことになる可能性は限りなく低くなります。
type Foo struct{}
func (f *Foo) Len() int { return 0 }
func (f *Foo) Get(i int) protoreflect.FileImport { return protoreflect.FileImport{} }
func (f *Foo) ProtoInternal(protoreflect.DoNotImplement) {}
では、パッケージの利用者側でFileImports
インターフェースを実装したい場合はどうするか?
これは簡単で、FileImports
をそのまま構造体に埋め込めこみます。コメントにある「許可されていないインターフェースの実装」というのは「意図しないインターフェースの実装」ととらえてよいかなと思います。
このような仕組みはなかなか自分では思いつけないですね...。スゴイ
type Foo struct {
protoreflect.FileImports
}
func (f *Foo) Len() int { return 0 }
func (f *Foo) Get(i int) protoreflect.FileImport { return protoreflect.FileImport{} }
(おまけ) NoUnkeyedLiterals
ここまでに DoNot***
を利用したテクニックを見てきましたが、protobufのpragmaパッケージにはもう一つ参考になるテクニックがあります。
それがNoUnkeyedLiterals
です。
NoUnkeyedLiterals
の実装は以下のようになっています。
(protobuf-go/internal/pragma/pragma.go)
// NoUnkeyedLiterals can be embedded in a struct to prevent unkeyed literals.
type NoUnkeyedLiterals struct{}
ただの構造体ですね。
コメントには以下の通り記載されています。
NoUnkeyedLiteralsは、キーを指定しないリテラルを防ぐために構造体に埋め込むことができます。
Goでは構造体のインスタンスを生成する際にキー名を指定せずにフィールドに値を設定できますが、NoUnkeyedLiteralsを埋め込むことでそれを防ぐことができるということです。
そもそもキー名を指定せずに構造体のインスタンスを生成することは、構造体の変更に弱くなるため推奨されていません。とくにライブラリの開発者の目線では、キー名を指定せずにインスタンスを生成することは将来の変更を考えてもやめてもらいたいことの一つです。
以下のような構造体Foo
とBar
で検証してみます。
NoUnkeyedLiteralsを埋め込んだBar
は、キー名、つまりname
やage
を指定せずにインスタンスを生成することはできません。
➡Try on Go Playground
type NoUnkeyedLiterals struct{}
type Foo struct {
name string
age int
}
type Bar struct {
NoUnkeyedLiterals
name string
age int
}
func main() {
fmt.Println(&Foo{"alice", 20}) // OK
fmt.Println(&Bar{"bob", 20}) // NG
}
./prog.go:22:19: cannot use "bob" (untyped string constant) as NoUnkeyedLiterals value in struct literal
./prog.go:22:26: cannot use 20 (untyped int constant) as string value in struct literal
./prog.go:22:28: too few values in struct literal of type Bar
まとめ
google.golang.org/protobufの中を読むことで、言語本来の仕様やリンタのチェック仕様をうまく活用するころで構造体やインターフェースに制約を加えていることがわかりました。こういう仕組みはなかなか自分では思いつけないので、他人のコードを読んで学ぶことの大切さをあらためて実感しました。