go/types に Identical/IdenticalIgnoreTags と整合するハッシュ関数を提供する Hasher・HasherIgnoreTags を追加
go/types
概要
go/types パッケージに Hasher と HasherIgnoreTags の2つの型が追加された。どちらも hash/maphash.Hasher インターフェースの実装であり、Type 値に対するハッシュ関数と同値関係のペアを提供する。Hasher は Identical と整合するハッシュを、HasherIgnoreTags は構造体タグを無視する IdenticalIgnoreTags と整合するハッシュを計算する。これにより、選んだ同値関係のもとで Type をハッシュテーブルなど連想データ構造のキーとして扱えるようになる。
導入経緯
本提案は当初、golang.org/x/tools/go/types/typeutil.Map のジェネリック版に相当する HashMap[V] 型そのものを go/types に追加するものだった。しかし議論の中で「Type 固有の部分はハッシュ関数だけで、汎用のハッシュテーブルは別に用意できるのではないか」という指摘が上がり、提案はハッシュ関数(Hasher/HasherIgnoreTags)の提供のみに縮小された。
その後、ハッシュ値をプロセスをまたいで安定させるべきか、シードを取るべきかという設計上の疑問が持ち上がり、汎用のハッシュインターフェース(hash/maphash.Hasher)を定める別提案(#70471)の決着を待つ形で一時保留(on hold)となった。#70471 が受理されたのを受けて API がそのインターフェースに整合する形に更新され、審査の結果、受理(accepted)された。
議論のハイライト
- 当初含まれていた汎用ハッシュテーブル型
HashMap[V]は、Typeに固有の部分がハッシュ関数のみであるという指摘を受け提案から外され、ハッシュ関数のみの提案に絞り込まれた。 - ハッシュ値の安定性について議論があり、将来ハッシュ関数の実装を変更する余地を残すため、プロセスをまたいだ安定性は約束しないことで合意した。
- ハッシュフラッディング攻撃への耐性が論点になったが、型チェッカーへの入力を制御できる攻撃者は他の手段でも性能に影響を与えられるため大きな懸念ではないとされ、最終的にシードの扱いは
hash/maphash.Hasherインターフェースの設計に委ねられた。 HasherIgnoreTagsを用意することで、構造体タグの有無によらず単一のハッシュ実装をIdentical/IdenticalIgnoreTagsの双方と整合させられるようにした。
使用例
Before
package main
import (
"fmt"
"go/types"
)
func typesEqual(a, b types.Type) bool {
return types.Identical(a, b)
}
func main() {
fmt.Println(typesEqual(types.Typ[types.Int], types.Typ[types.Int]))
}
After
package main
import (
"fmt"
"go/types"
"hash/maphash"
)
func hashOf(h types.Hasher, seed maphash.Seed, t types.Type) uint64 {
var mh maphash.Hash
mh.SetSeed(seed)
h.Hash(&mh, t)
return mh.Sum64()
}
func main() {
var h types.Hasher
seed := maphash.MakeSeed()
a := types.Typ[types.Int]
b := types.Typ[types.Int]
fmt.Println(hashOf(h, seed, a) == hashOf(h, seed, b), h.Equal(a, b))
}
実装解説
src/go/types/hash.go の実装によると、Hasher/HasherIgnoreTags はどちらも内部の hasher.hash メソッドに委譲する薄いラッパーで、実際のハッシュ計算ロジックは共通である。両者は書き込む先頭バイトが 0/1 で異なるだけで(Hash メソッド)、Equal メソッドがそれぞれ Identical/IdenticalIgnoreTags を呼び分ける。
hasher.hash は Type の具体的な種類(*Struct、*Named、*Interface 等)ごとに Identical と同じ構造を辿ってハッシュへ書き込む。構造体フィールドのハッシュではタグ(t.Tag(i))を意図的に読み飛ばしており、これが同一のハッシュ関数を Identical と IdenticalIgnoreTags の両方に使い回せる理由になっている。また、ジェネリック関数シグネチャ内の型パラメータは差分の少ないインデックスでハッシュする一方、シグネチャ外の自由な型パラメータはオブジェクト同一性に基づくより判別性の高いハッシュを使うなど、Identical の比較規則に対応した最適化が随所に見られる。
この2つの型が実装する Hasher[T any] インターフェース(Hash/Equal の2メソッド)は src/hash/maphash/hasher.go で定義されており、シードによるハッシュフラッディング対策の枠組みそのものは go/types 側ではなく hash/maphash パッケージ側が担う。