📝

Goでstructが特定のinterfaceを実装してることをコンパイル時にチェックさせる方法

2022/10/13に公開

前提条件

本記事に記載したコードはGo 1.19で動作確認したものである.

抱えていた問題

例えば以下のようなコードを書いたとする.

package main

import "fmt"

type A struct{}
type I interface {
	F()
}

func (a *A) F() {
}

func f(i I) {
}

func main() {
	f(&A{})
	fmt.Println("Hello, 世界")
}

この時,struct A が interface Iを実装しているか保証する手段がなく,コンパイル時まで気付けない.

以前はテストコードでstruct A が interface I 型の変数に代入できるかどうかをチェックするテストを手動で書いていた.しかし関数 f に渡したいstructがどんどん増えると [1] 型チェックをするためのテストケースを毎回書くコストが一生増え続けてしまう.また,テストケースを追加し忘れると危険である.

例えばJavaのように,特定のクラスが特定のinterfaceを実装していることを宣言して明示的に実装させる方法があればコンパイル時にチェックできるのだが,Goにはそのような機能が存在しない.

解決策

という悩みをtwitterに書いたらコンパイル時にチェックさせる方法を教えてもらった.

このアドバイスに従うと以下のようなコードを書くことで常にstruct A がinterface I を実装していることをコンパイル時にチェックできる.

package main

import "fmt"

type A struct{}
type I interface {
	F()
}

func (a *A) F() {
}

func f(i I) {
}

var _ I = (*A)(nil) // 新規追加

func main() {
	f(&A{})
	fmt.Println("Hello, 世界")
}

nilを struct Aのポインタにキャストして I 型の変数へのキャストをコンパイル時に実行することで,struct A がinterface Iを実装しているかどうかをコンパイル時にチェックできる.

仮に実装していない場合,コンパイル時に以下のようなエラーになる.

./prog.go:15:11: cannot use (*A)(nil) (value of type *A) as type I in variable declaration:
	*A does not implement I (missing F method)

知らなかったのだが,このテクニックは割と有名らしくググると同じ方法を紹介しているサイトがいくつかヒットする.下記のStackoverflowの質問でも同じ方法が回答として提案されている.

https://stackoverflow.com/questions/27803654/explanation-of-checking-if-value-implements-interface

余談:ポインタのInterface型変数への代入について

ちなみに, var _ I = (*A)(nil) の左辺は Iじゃなくて*I ではないかと思ったが,Goにおいてはポインタはinterface型の変数に代入できるらしい.もちろんinterfaceのポインタも作れるが,それは別物として扱われるようだった.

実際に左辺を *I に変更してみると以下のエラーになる.

./prog.go:15:12: cannot use (*A)(nil) (value of type *A) as type *I in variable declaration:
	*A does not implement *I (type *I is pointer to interface, not interface)

イマイチ理由がピンと来ないのでGoのinterfaceと型解決の関係性についてはまた別途調べたい.Goのinterfaceは,自分が経験のあるJavaのinterfaceやRustのtraitとも少しずつ違うのでまだ理解が不十分な所がある.

脚注
  1. 同じmodule内にあればコンパイル時に気付きやすいが,渡されるStructが別moduleで定義されていてかつ別々にビルドされているような場合だとたまに困る. ↩︎

Discussion