はじめに
初めまして、J.B.GoodeでエンジニアをしているFujimotoです!
今回はGoの、「インターフェースガード」についてまとめていこうと思います。
構造体がインターフェースを満たしているかどうかは、そのインターフェース型の変数へ代入されるタイミングで初めて検証されます。これはダックタイピングに近い柔軟さをもたらす一方で、実装漏れの発見が遅れるリスクも伴います。
インターフェースガード(Interface Guard)は、このリスクをコンパイル時に排除する1行のイディオムです。
var _ InterfaceName = (*StructName)(nil)
本記事では、このイディオムの仕組み・必要性・実践的な使い方を解説していこうと思います。
なぜGoのインターフェースは暗黙的なのか
インターフェースガードの話に入る前に、そもそもGoがなぜ implements を持たず、暗黙的なインターフェースを採用しているのかを整理してみます。
Javaなどの言語では、実装者側が「このインターフェースを実装します」と明示的に宣言します。一方Goでは、消費者側(そのインターフェースを使う側)が「このメソッドを持っていれば受け入れます」と定義し、実装者側は何も宣言しません。
この設計には明確な目的があります。
消費者側で必要最小限のインターフェースを定義することで、パッケージ間の結合を最小限(疎結合)にするということです。
たとえば io.Reader は Read(p []byte) (n int, err error) という1メソッドだけを持つインターフェースですが、*os.File も *bytes.Buffer も、すべて暗黙的にこれを満たします。
実装者側が io.Reader の存在を知っている必要すらありません。
この柔軟さはGoの大きな強みですが、トレードオフとして「構造体がインターフェースを満たしているかどうか」がコード上で明示されません。
つまり、次のような必要性が言えるでしょう。
なぜインターフェースガードが必要なのか
インターフェースガードの必要性は、私的に以下の3つが挙げられると思います。
1. 暗黙的な実装がもたらす落とし穴
先述した通り、Goのインターフェースは暗黙的に満たされます。
type UserRepository interface {
FindByID(id int64) (*User, error)
}
type userRepo struct{ db *sql.DB }
// メソッド名の typo: "Id" ≠ "ID"
func (r *userRepo) FindById(id int64) (*User, error) {
// ...
}
この userRepo は UserRepository を満たしていません。しかしエラーになるのは、実際に UserRepository 型の変数へ代入するコード(Service層やDIコンテナなど)がコンパイルされたときだけです。代入箇所がテストでしかカバーされていなければ、CIを回すまで気づけません。
2. リファクタリング時の安全ネット
インターフェースに新しいメソッドを追加した場合を考えてみましょう。
type UserRepository interface {
FindByID(id int64) (*User, error)
Delete(id int64) error // 新規追加
}
インターフェースガードを仕込んでおけば、Delete が未実装の構造体はその定義ファイルで即座にコンパイルエラーになります。わざわざファイルを探し回らなくても修正すべきファイルをすぐ見つけることができます。
3. 引数・戻り値のシグネチャ不一致
メソッド名が合っていても、引数や戻り値の型が一致しなければインターフェースは満たされません。
// インターフェース側
Store(ctx context.Context, user *entity.User) error
// 実装側 ― context.Context を受け取り忘れてしまっている
Store(user *entity.User) error
こうしたシグネチャの不一致もインターフェースガードで即座に検出できます。
構文の解説
以下を例に考えてみましょう。
var _ usecase.UserRepository = (*userRepo)(nil)
この1行は3つの要素で構成されています。それぞれ見ていきましょう。
| 要素 | 記述 | 役割 |
|---|---|---|
| ブランク識別子 | _ | 変数を実際には使用しないことを示す、かつ、未使用変数のコンパイルエラーを防ぐ時に使用します。 |
| インターフェース型 | usecase.UserRepository | 左辺の型がきます。代入互換性がここで検証されます。 |
| nilポインタキャスト | (*userRepo)(nil) | *userRepo 型のゼロ値(nil)。インスタンスを生成しないためメモリ割り当てが発生しない |
コンパイラは「*userRepo は usecase.UserRepository に代入可能か?」を検証し、不足メソッドがあればエラーを出します。実行時コストはゼロです。
💡 ブランク識別子 _ の補足
Goでは未使用のローカル変数はコンパイルエラーになります。_ を使えば「値を受け取るが使わない」ことを明示できます。
// 例: 2番目の戻り値を破棄
user, _ := repo.FindByID(1)
インターフェースガードでは、型チェックのためだけに変数を宣言したいので _ が最適です。
【実践例】リポジトリパターンでの活用
以下はユーザー管理のリポジトリ層を例にしたコードです。
インターフェース定義(UseCase層)
type UserRepository interface {
Store(ctx context.Context, user *entity.User) error
FindByID(ctx context.Context, id int64) (*entity.User, error)
FindByEmail(ctx context.Context, email string) (*entity.User, error)
}
このような実装するインターフェースを定義しておきます。
実装(Repository層)
次に、各インターフェースについてそれぞれ実装します。
DB操作について仮にSQLBoilerを使った場合は、次のようになるかと思います。
modelsパッケージに、SQLBoilerで生成されたGoファイルがあると思って以下のコードを読んでいただければと思います。
// コンパイル時にインターフェースの実装を保証する
var _ usecase.UserRepository = (*userRepository)(nil)
type userRepository struct {
db *sql.DB
}
func NewUserRepository(db *sql.DB) usecase.UserRepository {
return &userRepository{db: db}
}
func (r *userRepository) Store(ctx context.Context, user *entity.User) error {
model := converter.UserEntityToModel(user)
if err := model.Insert(ctx, r.db, boil.Infer()); err != nil {
return err
}
user.ID = model.ID
return nil
}
func (r *userRepository) FindByID(ctx context.Context, id int64) (*entity.User, error) {
model, err := models.Users(
models.UserWhere.ID.EQ(id),
).One(ctx, r.db)
if err != nil {
return nil, err
}
return converter.UserModelToEntity(model), nil
}
func (r *userRepository) FindByEmail(ctx context.Context, email string) (*entity.User, error) {
model, err := models.Users(
models.UserWhere.Email.EQ(email),
).One(ctx, r.db)
if err != nil {
return nil, err
}
return converter.UserModelToEntity(model), nil
}
この状態で FindByEmail を削除したり、シグネチャを変更すると、var _ の行で即座にコンパイルエラーが発生します。
値レシーバとポインタレシーバの違い
インターフェースガードの記述は、レシーバの種類によって変わります。
// ポインタレシーバで実装している場合
var _ MyInterface = (*MyStruct)(nil)
// 値レシーバで実装している場合(どちらでもOK)
var _ MyInterface = (*MyStruct)(nil) // ポインタ型でも可
var _ MyInterface = MyStruct{} // 値型でも可
ポインタレシーバのメソッドは値型からは呼び出せないため、ポインタレシーバを含む場合は必ず (*MyStruct)(nil) を使いましょう。
値レシーバのみであればどちらでも良いですが、統一性のために常にポインタ型で書くプロジェクトがほとんどだと思います。
まとめ
インターフェースガードは、Goの暗黙的インターフェース実装がもたらすリスクを1行で排除できるイディオムです。このイディオムはGoコミュニティで広く認知されており、標準ライブラリや有名プロジェクトでも使われています。
- コンパイル時にメソッドの欠落やシグネチャの不一致を検出できる
- 実行時コストはゼロ(nilポインタの代入のみ)
- リファクタリングやインターフェース変更時の安全ネットとして機能する
構造体がインターフェースを実装するファイルには、先頭に var _ InterfaceName = (*StructName)(nil) を置きましょう。
この1行の習慣が、実行時パニックやデバッグの時間を確実に減らしてくれるので使っていきましょう!
We’re Hiring !!
J.B.Goode Inc.のウェブサイトでは、技術記事の他にも技術ナレッジや日々の気づき等を配信しています。
カジュアル面談も実施中です。お気軽にお問い合わせください。
https://www.jbgoode.jp/recruit/