Go:型アサーションでインターフェースへの準拠を確認する
最近久しぶりにこういう↓イディオムを見かけ、色々文脈があるなと感じたのでちゃんと調べてみた。
type A struct {} var _ SampleType = (*A)(nil)
uber-goのスタイルガイドの「Verify Interface Compliance」よりソースコードを抜粋し、説明をまとめる。
type Handler struct { // ... } var _ http.Handler = (*Handler)(nil) // ←これ func (h *Handler) ServeHTTP( w http.ResponseWriter, r *http.Request, ) { // ... }
*Handler が http.Handler インターフェースと一致しなくなると、var _ http.Handler = (*Handler)(nil)
はコンパイルに失敗する。
代入の右側 (*Handler)(nil)
は、アサートされた型のゼロ値。これは、ポインター型 (*Handler など)、スライス、マップの場合は nil であり、構造体型の場合は空の構造体 (例:var _ http.Handler = LogHandler{}
)。そのため、メモリはアロケートされない。
この記述を追加することを議論したuber-goのイシューによると、このイディオムは、Javaの implements
, Rustの impl ... for
に相当する。APIの契約の一部として特定のインターフェースを満たす必要がある公開型がある場合、型が予期されるインターフェイスのセットを実装していることをアサートし、メソッド シグネチャの変更による破損を防ぐ。 Newから始まる名前を持つコンストラクタがインターフェースを返す場合はこの1行は必要ないが、特に外部へインターフェースを公開するパッケージではインターフェースの実装を変更することでユーザーのコードを壊さないために、構造体を返すことが良しとされていることから、インターフェースへの準拠をこの1行で担保する。また、満たすべき条件を明示し、ドキュメントの追加形式として機能させることでコードが読みやすくなるとのこと。
まとめると、この var _ SampleType = (*A)(nil)
イディオムは具象型がインターフェースを満たしていることをコンパイル時に保証するためのもの。Goのインターフェースは暗黙に満たされるものであるため、インターフェースを実装する側がインターフェースへの準拠を保証するために使う、ということだと思っている。
アプリケーション開発時のこのコードのニーズについてmattnさんが説明してくれていた。
一方で業務で複数人で開発する場合、誰かがインタフェースにメソッドを追加した際に、そのインタフェースを実装していない struct はコンパイルエラーにしたい。こういうニーズの為に上記のおまじないはある。
— mattn (@mattn_jp) 2022年9月7日