interface{} を使う理由をレビューでどう見抜くか
interface{} を使う理由をレビューでどう見抜くか
Go言語における interface{} は、任意の型を受け取れる「完全抽象型」です。
柔軟さの裏側に、型安全性・責務の曖昧化・設計意図の不透明化という課題が隠れています。
この記事ではレビューアー視点で
- interface{} 使用の設計意図の読み解き方
 - 妥当性の評価基準
 - 具体的なレビューコメント事例
 
を順に整理していきます。レビューアー育成教材として、実務レビューと同等の視点で書きます。
良い実装例から設計意図を読み解く
まずは、正しくinterface{}を使うケースを整理します。
1. JSONライブラリにおける汎用パーサー
package parser
import (
    "encoding/json"
    "io"
)
// DecodeJSONは任意の構造体にJSONをデコードする汎用関数
func DecodeJSON(r io.Reader, v interface{}) error {
    return json.NewDecoder(r).Decode(v)
}このコードは、標準ライブラリでもよく見られる典型です。
ポイントは以下です:
- データの具体型は利用者側で決定
 - DecodeJSON自体は汎用的なI/O処理に集中
 - interface{}は「呼び出し側が実型を知っている」前提の抽象
 
汎用ライブラリの入出力で 「型を受け取る立場」の設計では、interface{}の利用は十分妥当です。
2. プラグイン実装のダイナミックロード
type Plugin interface {
    Execute(args interface{}) error
}この例は、プラグイン拡張ポイントを設計する際の柔軟性重視パターンです。
- 実装側が自由にargs構造を決められる
 - コア側は一切型を固定しない
 - interface{}は「型を知らない前提」の抽象化
 
「制約しないことが要件」の場合、interface{}が有効。型安全性は各実装側へ委譲する設計意図です。
良くない実装例
ここからは現場でよく見る誤用例に、レビューアー視点の指摘を加えて解説します。
例1: 特定用途なのに任意型を使ってしまう
func SaveUser(data interface{}) {
@Reviewer特定型(User型)にキャスト前提で設計されています。最初からUser型を引数に取る関数にしましょう。interface{}は不要です。> func SaveUser(u User) {>   saveToDB(u)> }    u := data.(User)
    saveToDB(u)
}例2: 動的分岐に逃げたロジック
func HandleEvent(e interface{}) {
    switch v := e.(type) {
    case UserCreated:
        handleUserCreated(v)
    case UserDeleted:
        handleUserDeleted(v)
    default:
        log.Println("unknown event")
    }
}
@Reviewerこの分岐はポリモーフィズム未活用の手続き的実装です。イベント型ごとにインターフェースを定義し、各型で共通ハンドラを実装しましょう。
例3: JSONマップ偏重
func ProcessData(input map[string]interface{}) {
    id := input["id"].(string)
    value := input["value"].(int)
    fmt.Printf("ID: %s Value: %d\n", id, value)
}
@Reviewer入力構造が固定されているなら構造体定義しましょう。マップ経由は型安全性も補完も犠牲にします。> type Data struct {>     ID    string `json:"id"`>     Value int    `json:"value"`> }
例4: ジェネリクス未活用の古い実装
func MapAny(slice []interface{}, f func(interface{}) interface{}) []interface{} {
    var result []interface{}
    for _, v := range slice {
        result = append(result, f(v))
    }
    return result
}func MapAny[T any](slice []T, f func(T) T) []T {
    var result []T
    for _, v := range slice {
        result = append(result, f(v))
    }
    return result
}Go1.18以降はジェネリクス対応が可能です。型安全にリファクタリングしましょう。
レビュー観点チェックリスト
| チェック項目 | 確認ポイント | 
|---|---|
| 任意型の妥当性 | 仕様として「型を知らない必要」があるか? | 
| 型アサーション乱用 | 特定型へのキャスト前提なら直に型宣言可能では? | 
| 型スイッチ依存 | interface分岐よりもポリモーフィズム設計可能では? | 
| マップ汎用構造 | map[string]interface{}は構造体定義で代替可能では? | 
| ジェネリクス活用 | Go1.18以降は型パラメータ導入で安全性向上可能では? | 
あとがき:柔軟さに逃げず、意図を固定せよ
interface{}はGoにおける最後の抽象逃げ場です。
「柔軟にしておいた方が後から楽になる」という誘惑に負けやすい構造ですが、レビューの本質はむしろ逆です。
- 柔軟さに逃げず
 - 型に意図を刻み
 - 抽象を構造化する
 
レビューアーは常に「未来の利用者の負荷」を考慮し、型安全・保守性・責務分離を軸に改善提案する技術的ファシリテーターを目指しましょう。