13
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

NRI OpenStandiaAdvent Calendar 2024

Day 5

【Goテクニック】DoNotCopy、DoNotCompare、DoNotImplement

Last updated at Posted at 2024-12-04

Go-Logo_Blue.png

この記事では、Go言語のgoogle.golang.org/protobufパッケージを利用しているときに見かけたDoNotCopyDoNotCompareDoNotImplementの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

DoNotCopyの検証コード
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

DoNotCompareの検証コード
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)。

  1. Struct types are comparable if all their field types are comparable.
  2. 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インターフェースなどで利用されています。

FileImportsのインターフェース定義
type doNotImplement pragma.DoNotImplement

type FileImports interface {
	Len() int
	Get(i int) FileImport
	doNotImplement
}

では、「インターフェースの安易な実装」とはどういう意味か、上記のFileImportsインターフェースを実装する構造体を定義してみます。
次のような構造体Fooを定義して、FileImportsインターフェースを実装することを試みましたが、この実装は機能しません。なぜならprotoreflect.DoNotImplementインターフェースがinternalパッケージに定義されているからです。加えてDoNotImplementインターフェースは自分自身を循環参照しているため、Foo構造体が知らない間にFileImportsインターフェースを実装していた!なんてことになる可能性は限りなく低くなります。

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をそのまま構造体に埋め込めこみます。コメントにある「許可されていないインターフェースの実装」というのは「意図しないインターフェースの実装」ととらえてよいかなと思います。
このような仕組みはなかなか自分では思いつけないですね...。スゴイ

Foo構造体に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を埋め込むことでそれを防ぐことができるということです。
そもそもキー名を指定せずに構造体のインスタンスを生成することは、構造体の変更に弱くなるため推奨されていません。とくにライブラリの開発者の目線では、キー名を指定せずにインスタンスを生成することは将来の変更を考えてもやめてもらいたいことの一つです。

以下のような構造体FooBarで検証してみます。
NoUnkeyedLiteralsを埋め込んだBarは、キー名、つまりnameageを指定せずにインスタンスを生成することはできません。
Try on Go Playground

NoUnkeyedLiteralsの検証コード
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の中を読むことで、言語本来の仕様やリンタのチェック仕様をうまく活用するころで構造体やインターフェースに制約を加えていることがわかりました。こういう仕組みはなかなか自分では思いつけないので、他人のコードを読んで学ぶことの大切さをあらためて実感しました。

参考資料

13
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?