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における最後の抽象逃げ場です。
「柔軟にしておいた方が後から楽になる」という誘惑に負けやすい構造ですが、レビューの本質はむしろ逆です。

  • 柔軟さに逃げず
  • 型に意図を刻み
  • 抽象を構造化する

レビューアーは常に「未来の利用者の負荷」を考慮し、型安全・保守性・責務分離を軸に改善提案する技術的ファシリテーターを目指しましょう。