Goのジェネリックライブラリでは、見た目上「空」な構造体に奇妙なフィールドを埋め込むことがある。それは長さ0の配列で、要素型は関数かつ型パラメータを持つ。これは単なるトリックではなく、コンパイラを使って誤使用を防ぐための技法である。
1. 実際のニーズから:プラグイン可能な「比較戦略」
例えば、スライスに対して重複除去や検索、比較を行うジェネリックツールを作成しているとする。ユーザーが「2つの要素が等しいかの判断方法」をカスタマイズできるようにしたい。
package main
type Comparator[T any] interface {
Compare(a, b T) bool
}
type Searcher[T any, C Comparator[T]] struct {
comp C
}
func (s Searcher[T, C]) FindIndex(xs []T, target T) int {
for i, v := range xs {
if s.comp.Compare(v, target) {
return i
}
}
return -1
}
デフォルト実装として、Tが比較可能(comparable)な場合に==を使うようにしたい。
type BasicComparator[T comparable] struct{}
func (BasicComparator[T]) Compare(a, b T) bool {
return a == b
}
ここまでで問題ないように見えるが、実際には2つの「型セーフティ」の落とし穴がある。
2. 落とし穴A:異なるTのデフォルト戦略が同じように見える
BasicComparator[int]とBasicComparator[string]はメモリレイアウト上空構造体(struct{})である。
空構造体の最大の特徴はフィールドがまったくないこと。そのため、多くの場合「完全に同じように見える」。
大規模なジェネリックラッパーを作成する際、以下のような状況が発生する可能性がある:
- デフォルト戦略をパラメータ、フィールド、戻り値として頻繁に渡す
- ラッパー/型エイリアス/反射/unsafeや中間抽象層での処理
- 「戦略タイプ」を「ただの空殻」と見なされた場合、誤って戦略を取得してしまうか、変換で回避してしまう可能性がある
直感的な理解:ジェネリック型がインスタンス化後も空である場合、型システムで制限できるものが少なくなり、誤使用の可能性が増える。
我々が望むのは:
BasicComparator[int]とBasicComparator[string]が「構造的に異なる」ようにすることで、できるだけエラーをコンパイル時に防ぐ。
3. 落とし穴B:戦略オブジェクトが比較される・マップのキーとして使われる
戦略オブジェクト(比較器、ハッシュ器、ソートルールなど)は通常、データを持たず、動作を担当する。 もしそれが比較可能な空構造体であれば、以下のような「見た目は合理だが危険な」コードがコンパイル可能になる。
// 例:戦略をキーとして結果をキャッシュ
map[BasicComparator[int]]something
より一般的には、コンテナやキャッシュ構造を実装し、後日誰かが戦略オブジェクトを構造体に詰め込み、==で全体を比較しようとした場合、比較が成功するが意味が完全に異なるバグが隠れている。
我々が望むのは:
戦略オブジェクトが==比較をサポートしないことで、比較を試みた際にコンパイルエラーになるようにする。
4. 解決策:ゼロバイトながら型に強く束縛されたフィールドを追加
デフォルト戦略を次のように変更する。
type SaferComparator[T comparable] struct {
_ [0]func(T)
}
func (SaferComparator[T]) Compare(a, b T) bool {
return a == b
}
このフィールドは2つの目的を果たす。
4.1 [0]...:ゼロ長配列でメモリコストゼロ(実行時コストなし)
[0]Xのサイズは常に0で、Xが何であれ。
このフィールドは構造体のサイズを大きくせず、割り当てコストも逃逸分析にも影響しない——これはほぼ「型システム専用」の仕組み。
4.2 func(T):関数型は比較不能 → 構造体も比較不能
Goでは:
- 関数値(
func(...))は比較不能な型 - 比較不能なフィールドを持つ構造体も比較不能
したがって:
var a, b SaferComparator[int]
// _ = (a == b) // コンパイルエラー:この型は比較不能
これにより、「戦略オブジェクトが比較される/キーとして使われる」という誤使用をコンパイル時に防ぐことができる。
4.3 func(T)にTを含める:型パラメータを構造に「烙印」
ポイントはfunc(T)というフィールド型が型パラメータTを含んでいること。
Tが異なるとフィールド型も異なる:
SaferComparator[int]のフィールドは[0]func(int)SaferComparator[string]のフィールドは[0]func(string)
構造レベルで「同じように見える」ことがなくなり、中間層で互換性のあるものとして扱われにくくなる(特に多くのラッパー、型エイリアス、ジェネリックアダプターを持つ場合、この「強い区別」は非常に価値がある)。
5. イメージしやすい対照実験
5.1 対照:空構造体の戦略は比較可能(通常望まない)
type PlainComparator[T comparable] struct{}
func (PlainComparator[T]) Compare(a, b T) bool {
return a == b
}
func compareStrategy() {
var x, y PlainComparator[int]
_ = (x == y) // コンパイル可能:しかし比較することは通常意味がない
}
5.2 _ [0]func(T)を追加後:比較が直接禁止される
type StrictComparator[T comparable] struct {
_ [0]func(T)
}
func (StrictComparator[T]) Compare(a, b T) bool {
return a == b
}
func compareStrategy2() {
var x, y StrictComparator[int]
// _ = (x == y) // コンパイルエラー:比較不能(より安全)
}
5.3 依然としてゼロコスト:オブジェクトサイズは変わらない(0のまま)
空構造体のサイズは0;[0]func(T)を追加しても0のまま。
(unsafe.Sizeofで確認可能:どちらも0;他の構造体に入れてもスペースを消費しない——ゼロ長配列はレイアウトに影響しない)
6. なぜ他の「Tを束縛する方法」を使わないのか?
あなたは以下の方法も考えているかもしれない:
6.1 _ T —— 不可能、スペースを消費し、値が必要
type Tag[T any] struct {
_ T
}
// Tのサイズを消費し、完全にゼロコストではない
6.2 _ *T —— ポインタサイズを消費
type Tag[T any] struct {
_ *T
}
// 通常8バイト(64ビット)
6.3 struct{} —— ゼロコストだがTへの束縛が弱い
type Tag[T any] struct {
_ struct{}
}
// 0コストだが、Tをフィールド型に「烙印」していない
[0]func(T)は以下の3点を同時に満たす:
- ゼロコスト(0バイト)
- 強いTへの束縛(フィールド型がTに依存)
- 構造体を比較不能に(関数が比較不能だから)
これは「1つのフィールドで3つの利益」を得る技法。
7. この技法をいつ使うべきか?
適用可能なシナリオ(非常に典型的):
- 戦略/設定/アダプタオブジェクト:Equal/Hash/Compare/Encode/Decode戦略など
==比較を望まない場合:比較は通常意味がなく、バグを隠す可能性がある- 異なる型パラメータのインスタンスが型レベルで強く区別されるようにしたい場合:多重ラッパーで「空殻として扱われるのを防ぐ」
不適切なシナリオ:
- 型の値を比較する必要がある(なら比較不能にしない)
- 型が実際のデータを保持する必要がある(ならフィールドを直接追加し、ラベルは不要)
8. 実際に使えるコード例(すべて新規)
package main
type Comparator[T any] interface {
Compare(a, b T) bool
}
type SaferComparator[T comparable] struct {
_ [0]func(T)
}
func (SaferComparator[T]) Compare(a, b T) bool {
return a == b
}
type Searcher[T any, C Comparator[T]] struct {
comp C
}
func (s Searcher[T, C]) Contains(xs []T, target T) bool {
for _, v := range xs {
if s.comp.Compare(v, target) {
return true
}
}
return false
}
func main() {
s := Searcher[int, SaferComparator[int]]{
comp: SaferComparator[int]{},
}
_ = s.Contains([]int{1, 2, 3}, 2)
}
この例では:
- 戦略タイプは0サイズ(ほぼゼロコスト)
- 戦略オブジェクトは比較不能(誤使用防止)
SaferComparator[int]とSaferComparator[string]は型レベルで強く区別される
9. 一言でまとめると
_ [0]func(T)は**「ゼロバイトフィールド + 強い型束縛 + 比較禁止」**の組み合わせテクニック。
非常に低コストで強力なコンパイル時制約を実現し、ジェネリックライブラリの「デフォルト戦略/型ラベル/動作アダプタ」に最適である。