コンテンツにスキップ

GoのinterfaceとGenericsの内部構造と進化

チェック

  • [ ] 本文を確認した
  • [ ] 概要を確認した
  • [ ] タグを確認した
  • [ ] inbox/ 直下へ移行した

概要

Go の型、interface{} / any、非空 interface、Generics の内部構造と使い分けを説明する Go Conference 2025 の資料。 interface{} は型情報と値ポインタを持つ runtime 構造であり、typed nil の落とし穴がある。 非空 interface は itab を介した動的ディスパッチとして実装される。 Generics は辞書と gcshape stenciling によって実装され、再利用性の高いデータ構造やアルゴリズムではコンパイル時の型安全性を得られる。

解説

Go の any は便利だが、型安全性をコンパイル時に捨てる選択でもある。 特に typed nil は、errorany を使った実装で実務上のバグになりやすい。 この資料は、なぜ var x any = (*T)(nil)nil と等しくないのかを runtime 表現から理解する材料になる。

Generics と interface の使い分けでは、「同じ処理を複数の型に対して型安全に行いたい」のか、「型ごとに異なる振る舞いをメソッド経由で呼びたい」のかを分けると判断しやすい。

本文

本文は Speaker Deck の transcript から取得できた内容をもとに、日本語で再構成したもの。

資料の目的

この資料の目的は、Go の型、interface、Generics の内部構造を理解し、正しく使い分けること。 Go 1.25.1 時点のソースコードに基づき、runtime、compiler、internal ABI の構造を参照している。

関係する主な内部パッケージは次の通り。

  • src/runtime: 実行時のスケジューラ、GC、interface 表現など
  • src/cmd/compile: コンパイラ本体
  • src/internal/abi: コンパイラと runtime が共有する ABI と低レベル型情報

型の内部実装

Go の型情報は runtime / internal ABI 側で構造体として表現される。 基本型だけでなく、配列、スライス、マップ、チャネル、関数、構造体などの派生型も、共通の型情報を持つ構造として扱われる。

この型情報にはサイズ、ポインタを含む範囲、hash、kind、等価性判定関数、GC 用データ、名前への参照などが含まれる。

interface{} / any の内部構造

空 interface は、型情報と値へのポインタを持つ。 概念的には次の 2 つで構成される。

eface {
  type metadata
  data pointer
}

interface{}nil と等しいのは、型情報と値ポインタの両方が nil のときだけ。 値ポインタが nil でも、型情報が入っていれば nil ではない。 これが typed nil の原因になる。

type MyError struct{}

func (*MyError) Error() string {
    return "error"
}

func returnsTypedNil() error {
    var err *MyError = nil
    return err
}

func main() {
    fmt.Println(returnsTypedNil() == nil) // false
}

この例では、error interface の中に *MyError という型情報が入っているため、値ポインタが nil でも interface 全体としては nil ではない。

非空 interface の内部構造

メソッドを持つ interface は、itab と値ポインタで表現される。 itab は、実体型と interface のメソッドセットを対応付けるテーブル。 メソッド呼び出しは itab を通じて runtime に動的ディスパッチされる。

空 interface と非空 interface は内部表現が異なる。 空 interface は型情報を直接持ち、非空 interface は itab を持つ。

Generics の内部実装

Go の Generics は、辞書と gcshape stenciling によって実装される。 コンパイラは具体的な型引数を使ってジェネリック関数や型をインスタンス化する。 一方で、完全なモノモーフィゼーションだけではなく、辞書を使うことで 1 つの関数インスタンスを複数の型で動かし、コードサイズの増加を抑える。

型制約チェックはコンパイル時に行われる。 制約を満たさない型引数を渡すと、インスタンス化に失敗する。

Generics 導入前後の変化

Generics 導入前は、複数の型を扱う抽象化に interface{} と型アサーションを使うことが多かった。 この方法では、具体的な型情報の確認が runtime に寄り、実装者の責任が大きい。

Generics 導入後は、型パラメータと型制約により、コンパイル時に型チェックを行える。 たとえば汎用的な Stack[T]Min[T] のような関数・データ構造を型安全に書ける。

使い分けのガイドライン

Generics が向いている場面。

  • スライス、マップ、チャネルなどコンテナ型を扱う操作
  • 汎用データ構造
  • 異なる型でも処理が同じ形になるアルゴリズム

interface が向いている場面。

  • ある型のメソッドを呼び出すだけでよい
  • 型ごとに実装が異なる
  • メソッドを持たない型に対して動的な処理を行う必要がある

要点

  • interface{} / any は型情報と値ポインタを持つため typed nil が起きる。
  • 非空 interface は itab を介してメソッド呼び出しを動的ディスパッチする。
  • Generics はコンパイル時の型安全性を高めるが、万能ではない。
  • 同じ処理を複数型に適用するなら Generics、型ごとに異なる振る舞いを呼ぶなら interface が自然。
  • any を使う前に、Generics や具体型で表せないかを検討する。

タグ

go #generics #interface #type-system #runtime #compiler