第10章: インターフェース - マシンレベル完全解説

学習目標

この章を終えると、以下ができるようになります:

  • インターフェースの内部構造(iface と eface)を理解できる
  • 型アサーションと型スイッチの実装メカニズムを説明できる
  • インターフェースのメモリレイアウトを理解できる
  • 動的ディスパッチの仕組みを理解できる
  • インターフェースを使った効率的なコード設計ができる

🔑 インターフェースとは何か - メモリの視点から

インターフェースは、Goにおける多態性(ポリモーフィズム)を実現する中核的な機能です。しかし、内部的には複雑なデータ構造として実装されています。

インターフェースの2つの内部構造

Goのランタイムでは、インターフェースは2種類の構造体で表現されます:

1. eface (empty interface)
   - interface{} や any
   - メソッドを持たないインターフェース

2. iface (non-empty interface)
   - メソッドを持つインターフェース
   - io.Reader、fmt.Stringer など

eface の内部構造

type eface struct {
    _type *_type      // 型情報へのポインタ(8バイト)
    data  unsafe.Pointer  // 実際のデータへのポインタ(8バイト)
}

メモリレイアウト:

eface のメモリ(16バイト):
┌──────────────────────┐
│ _type: *_type        │  0-7バイト: 型情報へのポインタ
├──────────────────────┤
│ data: unsafe.Pointer │  8-15バイト: データへのポインタ
└──────────────────────┘

💡 重要: 空インターフェースは、型情報とデータの2つのポインタで構成される16バイトの構造体です。

iface の内部構造

type iface struct {
    tab  *itab        // インターフェーステーブルへのポインタ(8バイト)
    data unsafe.Pointer  // 実際のデータへのポインタ(8バイト)
}

type itab struct {
    inter *interfacetype  // インターフェース型情報
    _type *_type          // 具体的な型情報
    hash  uint32          // _type.hash のコピー
    _     [4]byte         // パディング
    fun   [1]uintptr      // 可変長のメソッドテーブル
}

メモリレイアウト:

iface のメモリ(16バイト):
┌──────────────────────┐
│ tab: *itab           │  0-7バイト: インターフェーステーブル
├──────────────────────┤
│ data: unsafe.Pointer │  8-15バイト: データへのポインタ
└──────────────────────┘

itab の構造:
┌──────────────────────┐
│ inter: *interfacetype│  インターフェース型情報
├──────────────────────┤
│ _type: *_type        │  具体的な型情報
├──────────────────────┤
│ hash: uint32         │  型のハッシュ値
├──────────────────────┤
│ fun: [...]uintptr    │  メソッドテーブル(関数ポインタの配列)
└──────────────────────┘

🔑 キーポイント: メソッドを持つインターフェースは、メソッドテーブル(vtable)を使って動的ディスパッチを実現します。

インターフェースの具体例 - メモリの動き

簡単な例

type Reader interface {
    Read(p []byte) (n int, err error)
}

type File struct {
    name string
}

func (f *File) Read(p []byte) (n int, err error) {
    // 実装
    return len(p), nil
}

func main() {
    var r Reader
    f := &File{name: "test.txt"}
    r = f  // インターフェースへの代入
}

メモリの変化:

ステップ1: var r Reader(nil インターフェース)

r のメモリ:
┌──────────────────────┐
│ tab: nil             │  ← nil
├──────────────────────┤
│ data: nil            │  ← nil
└──────────────────────┘

ステップ2: f := &File{name: "test.txt"}(具体的な値)

ヒープ:
┌──────────────────────┐
│ File{                │
│   name: "test.txt"   │  ← 実際の File 構造体
│ }                    │
└──────────────────────┘
        ↑
        │
スタック:
┌──────────────────────┐
│ f: 0x8000            │  ← File へのポインタ
└──────────────────────┘

ステップ3: r = f(インターフェースへの代入)

r のメモリ:
┌──────────────────────┐
│ tab: *itab           │  ← Reader と *File の組み合わせを示す itab
├──────────────────────┤
│ data: 0x8000         │  ← File インスタンスへのポインタ
└──────────────────────┘
        │
        │ tab が指す itab:
        ↓
┌──────────────────────┐
│ inter: *Reader型情報 │
├──────────────────────┤
│ _type: **File型情報  │
├──────────────────────┤
│ hash: 0xABCD1234     │
├──────────────────────┤
│ fun[0]: &File.Read   │  ← Read メソッドへの関数ポインタ
└──────────────────────┘

💡 重要な観察:

  • インターフェースは常に16バイト(2つのポインタ)
  • 実際のデータは別の場所に存在
  • メソッドテーブル(itab)は型の組み合わせごとに1つだけ作成される

動的ディスパッチ - メソッド呼び出しの仕組み

静的ディスパッチ vs 動的ディスパッチ

// 静的ディスパッチ(直接呼び出し)
f := &File{name: "test.txt"}
f.Read(buffer)  // コンパイル時にアドレスが決定

// 動的ディスパッチ(インターフェース経由)
var r Reader = f
r.Read(buffer)  // 実行時にメソッドテーブルから検索

動的ディスパッチのアセンブリ

r.Read(buffer)

このコードは、以下のような機械語に変換されます:

; r のアドレスを R1 にロード
MOVQ    r, R1

; itab のアドレスを取得(r.tab)
MOVQ    0(R1), R2

; メソッドテーブルから Read の関数ポインタを取得
; itab.fun[0] にアクセス(オフセット32バイト)
MOVQ    32(R2), R3

; data(実際の File インスタンス)を取得
MOVQ    8(R1), R4

; メソッドを呼び出し(R3 = 関数ポインタ、R4 = レシーバ)
; 引数: R4 (レシーバ), buffer
CALL    R3

⚠️ パフォーマンス注意: 動的ディスパッチは、静的ディスパッチに比べて:

  • メモリアクセスが2-3回増える
  • 分岐予測が難しくなる
  • インライン化ができない

しかし、柔軟性とのトレードオフとして、多くの場合許容できるオーバーヘッドです。

空インターフェース(interface{})の特殊性

空インターフェースの使用

var i interface{}

i = 42
i = "hello"
i = []int{1, 2, 3}
i = struct{ X int }{X: 10}

各代入時のメモリ状態:

i = 42

i (eface):
┌──────────────────────┐
│ _type: *int型情報    │
├──────────────────────┤
│ data: 0x8000         │  → ヒープまたはスタック上の int(42)
└──────────────────────┘

i = "hello"

i (eface):
┌──────────────────────┐
│ _type: *string型情報 │
├──────────────────────┤
│ data: 0x8010         │  → 文字列データへのポインタ
└──────────────────────┘
                │
                ↓
ヒープ:
┌──────────────────────┐
│ string header:       │
│   ptr: 0x9000        │  → 実際の "hello" バイト列
│   len: 5             │
└──────────────────────┘

i = []int{1, 2, 3}

i (eface):
┌──────────────────────┐
│ _type: *[]int型情報  │
├──────────────────────┤
│ data: 0x8020         │  → スライスヘッダへのポインタ
└──────────────────────┘
                │
                ↓
ヒープ:
┌──────────────────────┐
│ slice header:        │
│   ptr: 0x9100        │  → 実際の配列データ
│   len: 3             │
│   cap: 3             │
└──────────────────────┘

💡 重要: 空インターフェースは、どんな型でも格納できますが、常に16バイトのオーバーヘッドがあります。

小さな値の最適化

Goのコンパイラは、小さな値に対して最適化を行います:

var i interface{} = 42

この場合、42は直接 eface.data に埋め込まれる可能性があります(ポインタではなく値として)。

最適化された eface:
┌──────────────────────┐
│ _type: *int型情報    │
├──────────────────────┤
│ data: 42 (直接埋込) │  ← ヒープ割り当てなし!
└──────────────────────┘

🔑 最適化条件:

  • 値が8バイト以下
  • ポインタ型でない
  • GCが追跡する必要がない

型アサーション - 内部メカニズム

安全な型アサーション

var i interface{} = "hello"
s, ok := i.(string)

内部処理:

// 疑似コード(実際のランタイム処理)
func typeAssert(iface eface, targetType *_type) (value, ok bool) {
    // 1. 型情報を比較
    if iface._type == targetType {
        return iface.data, true
    }
    return nil, false
}

アセンブリレベル:

; i のアドレスを R1 にロード
MOVQ    i, R1

; i._type を取得
MOVQ    0(R1), R2

; 目的の型(string)の型情報アドレスを R3 にロード
LEAQ    type.string, R3

; 型を比較
CMPQ    R2, R3
JNE     assertion_failed

; 型が一致した場合、データを取得
MOVQ    8(R1), R4    ; i.data → R4
MOVQ    $1, R5       ; ok = true

JMP     assertion_done

assertion_failed:
    XORQ    R4, R4       ; value = nil
    XORQ    R5, R5       ; ok = false

assertion_done:
    ; R4 = value, R5 = ok

💡 パフォーマンス: 型アサーションは、ポインタ比較(高速)+ 条件分岐で実装されています。

危険な型アサーション

var i interface{} = 42
s := i.(string)  // panic!

この場合、okをチェックしないため、型が一致しない場合はパニックが発生します:

// 疑似コード
func typeAssertPanic(iface eface, targetType *_type) value {
    if iface._type != targetType {
        panic("interface conversion: " +
              iface._type.name + " is not " +
              targetType.name)
    }
    return iface.data
}

⚠️ ベストプラクティス: 常に value, ok := interface.(Type) の形式を使用しましょう。

型スイッチ - 効率的な実装

型スイッチの基本

func describe(i interface{}) {
    switch v := i.(type) {
    case int:
        fmt.Printf("Integer: %d\n", v)
    case string:
        fmt.Printf("String: %s\n", v)
    case []int:
        fmt.Printf("Int slice: %v\n", v)
    default:
        fmt.Printf("Unknown type: %T\n", v)
    }
}

型スイッチのコンパイル

型スイッチは、最適化されたジャンプテーブルまたはバイナリサーチに変換されます:

// コンパイラによる変換(疑似コード)
func describe_compiled(i interface{}) {
    ifaceType := i._type
    ifaceData := i.data

    // 型のハッシュ値でジャンプテーブルを使用
    switch ifaceType.hash {
    case intTypeHash:
        if ifaceType == intType {
            v := *(*int)(ifaceData)
            fmt.Printf("Integer: %d\n", v)
            return
        }
    case stringTypeHash:
        if ifaceType == stringType {
            v := *(*string)(ifaceData)
            fmt.Printf("String: %s\n", v)
            return
        }
    case sliceIntTypeHash:
        if ifaceType == sliceIntType {
            v := *(*[]int)(ifaceData)
            fmt.Printf("Int slice: %v\n", v)
            return
        }
    default:
        fmt.Printf("Unknown type: %T\n", ifaceType.name)
    }
}

💡 最適化:

  • ケースが少ない場合: 線形探索
  • ケースが多い場合: ハッシュテーブルまたはバイナリサーチ
  • 型のハッシュ値を使った高速比較

インターフェースと nil - よくある落とし穴

nil インターフェースの定義

var r io.Reader  // nil インターフェース

メモリ状態:

r (iface):
┌──────────────────────┐
│ tab: nil             │  ← nil
├──────────────────────┤
│ data: nil            │  ← nil
└──────────────────────┘

r == nil  → true

nil ポインタを持つインターフェース

var f *File = nil
var r io.Reader = f  // nil ポインタを持つインターフェース

メモリ状態:

r (iface):
┌──────────────────────┐
│ tab: *itab           │  ← nil ではない!
├──────────────────────┤
│ data: nil            │  ← nil ポインタ
└──────────────────────┘

r == nil  → false (!!)

⚠️ 重要な落とし穴:

func returnsError() error {
    var err *MyError = nil
    return err  // nil ではない!
}

func main() {
    if err := returnsError(); err != nil {
        fmt.Println("Error occurred!")  // これが実行される!
    }
}

理由:

返される error インターフェース:
┌──────────────────────┐
│ tab: *itab           │  ← *MyError の型情報
├──────────────────────┤
│ data: nil            │  ← nil ポインタ
└──────────────────────┘

err != nil → true(tab が nil でないため)

解決策:

func returnsError() error {
    var err *MyError = nil
    if err == nil {
        return nil  // 明示的に nil を返す
    }
    return err
}

これにより:

返される error インターフェース:
┌──────────────────────┐
│ tab: nil             │  ← nil
├──────────────────────┤
│ data: nil            │  ← nil
└──────────────────────┘

err == nil → true(正しい)

インターフェースのメソッドディスパッチテーブル

itab のキャッシュとメソッドテーブル

Goランタイムは、型とインターフェースの組み合わせごとに itab を生成し、キャッシュします。

type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
    Write(p []byte) (n int, err error)
}

type ReadWriter interface {
    Reader
    Writer
}

type File struct {
    name string
}

func (f *File) Read(p []byte) (n int, err error) { return }
func (f *File) Write(p []byte) (n int, err error) { return }

メモリ内の itab キャッシュ:

itab キャッシュ(グローバル):
┌─────────────────────────────────────┐
│ (*File, Reader) → itab1            │
│   itab1.fun[0] = &File.Read        │
├─────────────────────────────────────┤
│ (*File, Writer) → itab2            │
│   itab2.fun[0] = &File.Write       │
├─────────────────────────────────────┤
│ (*File, ReadWriter) → itab3        │
│   itab3.fun[0] = &File.Read        │
│   itab3.fun[1] = &File.Write       │
└─────────────────────────────────────┘

💡 最適化:

  • itab は初回使用時に生成され、その後はキャッシュから取得
  • 同じ型とインターフェースの組み合わせは、プログラム全体で1つの itab を共有
  • メモリ効率が良い

メソッドの順序

インターフェースのメソッドは、名前のアルファベット順でメソッドテーブルに配置されます:

type MultiMethod interface {
    Zebra()
    Alpha()
    Beta()
}

// itab.fun の配列:
// fun[0] = &Type.Alpha   ← 'A' が最初
// fun[1] = &Type.Beta    ← 'B' が2番目
// fun[2] = &Type.Zebra   ← 'Z' が最後

これにより、コンパイラとランタイムは、メソッドを一貫した順序で検索できます。

インターフェースのパフォーマンス最適化

1. インターフェース回避(可能な場合)

// 遅い: インターフェース経由
func processReader(r io.Reader) {
    buffer := make([]byte, 1024)
    for {
        n, err := r.Read(buffer)  // 動的ディスパッチ
        if err == io.EOF {
            break
        }
        // 処理
    }
}

// 速い: 具体的な型を使用
func processFile(f *os.File) {
    buffer := make([]byte, 1024)
    for {
        n, err := f.Read(buffer)  // 静的ディスパッチ
        if err == io.EOF {
            break
        }
        // 処理
    }
}

💡 トレードオフ: パフォーマンスと柔軟性のバランスを考慮しましょう。

2. 型アサーションによる最適化

func optimizedProcess(r io.Reader) {
    // 具体的な型にアサーションして最適化
    if f, ok := r.(*os.File); ok {
        // os.File 専用の高速処理
        processFileFast(f)
        return
    }

    // 汎用処理(遅い)
    processReaderGeneric(r)
}

3. 空インターフェースの回避

// 遅い: 空インターフェース使用
func processAny(items []interface{}) {
    for _, item := range items {
        // 型アサーションが必要(遅い)
        if v, ok := item.(int); ok {
            fmt.Println(v * 2)
        }
    }
}

// 速い: ジェネリクス使用(Go 1.18+)
func processGeneric[T any](items []T) {
    for _, item := range items {
        // 型が静的に決定される(速い)
        fmt.Println(item)
    }
}

標準ライブラリのインターフェース - 実装パターン

io.Reader と io.Writer

type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
    Write(p []byte) (n int, err error)
}

設計の美しさ:

  • メソッドが1つだけ(小さいインターフェース)
  • 多くの型が実装(os.File、bytes.Buffer、strings.Reader など)
  • 合成可能(io.Copy、io.TeeReader など)

カスタムReader の実装

type UppercaseReader struct {
    reader io.Reader
}

func (ur *UppercaseReader) Read(p []byte) (n int, err error) {
    n, err = ur.reader.Read(p)
    for i := 0; i < n; i++ {
        if p[i] >= 'a' && p[i] <= 'z' {
            p[i] = p[i] - 32  // 大文字に変換
        }
    }
    return n, err
}

使用例:

file, _ := os.Open("input.txt")
upperReader := &UppercaseReader{reader: file}

// io.Reader として使用可能
io.Copy(os.Stdout, upperReader)

メモリ図:

upperReader (*UppercaseReader):
┌──────────────────────────┐
│ reader: io.Reader        │  ← インターフェース(16バイト)
│   tab: *itab             │
│   data: *os.File         │  → 実際の os.File インスタンス
└──────────────────────────┘

io.Copy に渡される時:
┌──────────────────────────┐
│ tab: *itab               │  ← (*UppercaseReader, io.Reader) の itab
├──────────────────────────┤
│ data: upperReader        │  → UppercaseReader インスタンス
└──────────────────────────┘

fmt.Stringer

type Stringer interface {
    String() string
}

自動呼び出し: fmt.Printlnfmt.Printf("%v") は、自動的に String() メソッドを呼び出します。

type Point struct {
    X, Y int
}

func (p Point) String() string {
    return fmt.Sprintf("(%d, %d)", p.X, p.Y)
}

func main() {
    p := Point{X: 10, Y: 20}
    fmt.Println(p)  // 自動的に p.String() が呼ばれる
}

内部処理:

// fmt.Println の疑似コード
func Println(a ...interface{}) {
    for _, arg := range a {
        // Stringer インターフェースをチェック
        if stringer, ok := arg.(fmt.Stringer); ok {
            printString(stringer.String())
        } else {
            // デフォルトのフォーマット
            printDefault(arg)
        }
    }
}

インターフェースの実践的なパターン

1. デコレーターパターン

type Logger struct {
    writer io.Writer
}

func (l *Logger) Write(p []byte) (n int, err error) {
    fmt.Printf("[LOG] Writing %d bytes\n", len(p))
    return l.writer.Write(p)
}

// 使用例
file, _ := os.Create("output.txt")
logger := &Logger{writer: file}

// Logger は io.Writer なので、どこでも使える
io.WriteString(logger, "Hello, World!")

2. アダプターパターン

// 関数型を io.Reader に変換
type ReaderFunc func(p []byte) (n int, err error)

func (rf ReaderFunc) Read(p []byte) (n int, err error) {
    return rf(p)
}

// 使用例
counter := 0
reader := ReaderFunc(func(p []byte) (int, error) {
    if counter >= 10 {
        return 0, io.EOF
    }
    p[0] = byte('A' + counter)
    counter++
    return 1, nil
})

// reader は io.Reader として使用可能
io.Copy(os.Stdout, reader)  // ABCDEFGHIJ

3. モックとテスト

// テスト用モック
type MockReader struct {
    data []byte
    pos  int
    err  error
}

func (m *MockReader) Read(p []byte) (n int, err error) {
    if m.err != nil {
        return 0, m.err
    }
    if m.pos >= len(m.data) {
        return 0, io.EOF
    }
    n = copy(p, m.data[m.pos:])
    m.pos += n
    return n, nil
}

// テスト
func TestProcessor(t *testing.T) {
    mock := &MockReader{
        data: []byte("test data"),
    }

    result := processReader(mock)
    // アサーション...
}

🔍 自己確認問題

問題1: インターフェースのメモリサイズ

次のコードの出力を予測してください:

type Reader interface {
    Read(p []byte) (n int, err error)
}

type File struct {
    name string
    size int64
}

func (f *File) Read(p []byte) (n int, err error) {
    return 0, nil
}

func main() {
    var r Reader
    f := &File{name: "test.txt", size: 1024}
    r = f

    fmt.Println(unsafe.Sizeof(r))
    fmt.Println(unsafe.Sizeof(f))
    fmt.Println(unsafe.Sizeof(*f))
}

解答

出力(64ビットシステム):

16
8
24

説明:

  • unsafe.Sizeof(r): インターフェースは常に16バイト(tab + data ポインタ)
  • unsafe.Sizeof(f): ポインタ型なので8バイト
  • unsafe.Sizeof(f): File 構造体は string(16バイト) + int64(8バイト) = 24バイト

問題2: nil インターフェース

次のコードの出力を予測してください:

type MyError struct {
    msg string
}

func (e *MyError) Error() string {
    return e.msg
}

func returnsError1() error {
    return nil
}

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

func main() {
    err1 := returnsError1()
    err2 := returnsError2()

    fmt.Println(err1 == nil)
    fmt.Println(err2 == nil)
}

解答

出力:

true
false

説明:

  • err1: 明示的に nil を返すため、インターフェースの tab と data が両方 nil → err1 == nil は true
  • err2: nil ポインタを返すが、型情報(MyError)が含まれるため、tab が nil でない → err2 == nil は false

修正方法:

func returnsError2Fixed() error {
    var err *MyError = nil
    if err == nil {
        return nil  // 明示的に nil を返す
    }
    return err
}

問題3: 型アサーション

次のコードを完成させて、インターフェースから具体的な型を取得してください:

func process(i interface{}) {
    // ここを実装:i が *os.File の場合、ファイル名を出力
    // それ以外の場合、"Not a file" を出力
}

func main() {
    file, _ := os.Create("test.txt")
    process(file)          // ファイル名を出力
    process("not a file")  // "Not a file" を出力
}

解答

func process(i interface{}) {
    if f, ok := i.(*os.File); ok {
        fmt.Println("File name:", f.Name())
    } else {
        fmt.Println("Not a file")
    }
}

// または型スイッチを使用
func processSwitch(i interface{}) {
    switch v := i.(type) {
    case *os.File:
        fmt.Println("File name:", v.Name())
    default:
        fmt.Println("Not a file")
    }
}

問題4: インターフェースの実装チェック

次の構造体が io.ReadWriter インターフェースを実装しているか、コンパイル時にチェックする方法を実装してください:

type Buffer struct {
    data []byte
}

func (b *Buffer) Read(p []byte) (n int, err error) {
    // 実装
    return 0, nil
}

func (b *Buffer) Write(p []byte) (n int, err error) {
    // 実装
    return len(p), nil
}

// ここにコンパイル時チェックを実装

解答

// コンパイル時にインターフェースの実装をチェック
var _ io.ReadWriter = (*Buffer)(nil)

// または
var _ io.Reader = (*Buffer)(nil)
var _ io.Writer = (*Buffer)(nil)

説明:

  • この行は実行時には何もしないが、コンパイル時に型チェックが行われる
  • Bufferio.ReadWriter を実装していない場合、コンパイルエラーになる
  • (Buffer)(nil) は nil ポインタを型変換したもの(値は不要)

問題5: 型スイッチの最適化

次のコードを最適化してください:

func process(i interface{}) string {
    if _, ok := i.(int); ok {
        return "int"
    } else if _, ok := i.(string); ok {
        return "string"
    } else if _, ok := i.(bool); ok {
        return "bool"
    } else if _, ok := i.([]int); ok {
        return "[]int"
    } else {
        return "unknown"
    }
}

解答

型スイッチを使用して最適化:

func processOptimized(i interface{}) string {
    switch i.(type) {
    case int:
        return "int"
    case string:
        return "string"
    case bool:
        return "bool"
    case []int:
        return "[]int"
    default:
        return "unknown"
    }
}

利点:

  • コードが読みやすい
  • コンパイラによる最適化(ジャンプテーブルやバイナリサーチ)
  • 複数の型アサーションを避けられる

問題6: インターフェース合成

次の要件を満たすインターフェースを定義してください:
  • ReadCloser: Read と Close メソッドを持つ
  • WriteCloser: Write と Close メソッドを持つ
  • ReadWriteCloser: Read、Write、Close メソッドを持つ

埋め込みを使って実装してください。

解答

type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
    Write(p []byte) (n int, err error)
}

type Closer interface {
    Close() error
}

// 埋め込みを使った合成
type ReadCloser interface {
    Reader
    Closer
}

type WriteCloser interface {
    Writer
    Closer
}

type ReadWriteCloser interface {
    Reader
    Writer
    Closer
}

// または標準ライブラリの定義を使用
import "io"

// io.ReadCloser, io.WriteCloser, io.ReadWriteCloser が既に定義されている

利点:

  • 小さいインターフェースを組み合わせる
  • 再利用性が高い
  • Goらしいインターフェース設計

問題7: メソッドセットの理解

次のコードはコンパイルされますか?

type Counter struct {
    value int
}

func (c Counter) GetValue() int {
    return c.value
}

func (c *Counter) Increment() {
    c.value++
}

type Getter interface {
    GetValue() int
}

type Incrementer interface {
    Increment()
}

func main() {
    c := Counter{value: 0}

    var g Getter = c       // ケース1
    var i Incrementer = c  // ケース2
}

解答

ケース1: コンパイル成功

  • CounterGetValue() メソッドを持つ(値レシーバー)
  • 値レシーバーのメソッドは、値と型両方のメソッドセットに含まれる

ケース2: コンパイルエラー

  • CounterIncrement() メソッドを持たない(*Counter のみが持つ)
  • ポインタレシーバーのメソッドは、ポインタ型のメソッドセットにのみ含まれる

修正:

var i Incrementer = &c  // *Counter を渡す

メソッドセットのルール:

型 T のメソッドセット = 値レシーバーのメソッドのみ
型 *T のメソッドセット = 値レシーバー + ポインタレシーバーのメソッド

問題8: 空インターフェーススライス

次のコードはコンパイルされますか?

func printAll(items []interface{}) {
    for _, item := range items {
        fmt.Println(item)
    }
}

func main() {
    numbers := []int{1, 2, 3}
    printAll(numbers)
}

解答

コンパイルエラーになります。

理由: []int[]interface{} は異なる型です。Go は暗黙的な型変換を行いません。

メモリレイアウトの違い:

[]int のメモリ:
┌─────────┬─────────┬─────────┐
│ ptr     │ len     │ cap     │  24バイト(ヘッダ)
└────┬────┴─────────┴─────────┘
     └─→ [1][2][3]              各要素8バイト

[]interface{} のメモリ:
┌─────────┬─────────┬─────────┐
│ ptr     │ len     │ cap     │  24バイト(ヘッダ)
└────┬────┴─────────┴─────────┘
     └─→ [iface1][iface2][iface3]  各要素16バイト

解決策:

func main() {
    numbers := []int{1, 2, 3}

    // 変換が必要
    items := make([]interface{}, len(numbers))
    for i, n := range numbers {
        items[i] = n
    }

    printAll(items)
}

// またはジェネリクスを使用(Go 1.18+)
func printAllGeneric[T any](items []T) {
    for _, item := range items {
        fmt.Println(item)
    }
}

func main() {
    numbers := []int{1, 2, 3}
    printAllGeneric(numbers)  // 変換不要
}

問題9: インターフェースのパフォーマンス

次の2つの実装のうち、どちらが速いですか?

// 実装1: インターフェース使用
func sum1(r io.Reader) int {
    total := 0
    buf := make([]byte, 1)
    for {
        n, err := r.Read(buf)
        if err == io.EOF {
            break
        }
        total += int(buf[0])
    }
    return total
}

// 実装2: 具体的な型使用
func sum2(b *bytes.Reader) int {
    total := 0
    buf := make([]byte, 1)
    for {
        n, err := b.Read(buf)
        if err == io.EOF {
            break
        }
        total += int(buf[0])
    }
    return total
}

解答

sum2 の方が高速です。

理由:

  • 静的ディスパッチ: sum2bytes.Reader.Read を直接呼び出す(コンパイル時にアドレスが決定)
  • インライン化: コンパイラは Read メソッドをインライン化できる可能性がある
  • 動的ディスパッチのオーバーヘッド回避: sum1 は itab 経由でメソッドを呼び出す必要がある

ベンチマーク例:

BenchmarkSum1-8    1000000    1200 ns/op
BenchmarkSum2-8    2000000     600 ns/op

トレードオフ:

  • sum1: 柔軟(任意の io.Reader を受け入れる)
  • sum2: 高速(bytes.Reader のみ)

ベストプラクティス: まずは柔軟性を優先し、パフォーマンスボトルネックが特定されたら最適化する。

問題10: カスタムインターフェースの設計

ファイルキャッシュシステムを設計してください。以下の要件を満たすインターフェースを定義し、実装してください:

要件:

  • データの取得(Get)
  • データの保存(Set)
  • データの削除(Delete)
  • キャッシュのクリア(Clear)

解答

// 小さいインターフェースに分割
type Getter interface {
    Get(key string) ([]byte, error)
}

type Setter interface {
    Set(key string, value []byte) error
}

type Deleter interface {
    Delete(key string) error
}

type Clearer interface {
    Clear() error
}

// 合成インターフェース
type Cache interface {
    Getter
    Setter
    Deleter
    Clearer
}

// インメモリ実装
type MemoryCache struct {
    data map[string][]byte
    mu   sync.RWMutex
}

func NewMemoryCache() *MemoryCache {
    return &MemoryCache{
        data: make(map[string][]byte),
    }
}

func (c *MemoryCache) Get(key string) ([]byte, error) {
    c.mu.RLock()
    defer c.mu.RUnlock()

    value, ok := c.data[key]
    if !ok {
        return nil, fmt.Errorf("key not found: %s", key)
    }

    // コピーを返す(元のデータを保護)
    result := make([]byte, len(value))
    copy(result, value)
    return result, nil
}

func (c *MemoryCache) Set(key string, value []byte) error {
    c.mu.Lock()
    defer c.mu.Unlock()

    // コピーを保存(元のデータを保護)
    data := make([]byte, len(value))
    copy(data, value)
    c.data[key] = data
    return nil
}

func (c *MemoryCache) Delete(key string) error {
    c.mu.Lock()
    defer c.mu.Unlock()

    delete(c.data, key)
    return nil
}

func (c *MemoryCache) Clear() error {
    c.mu.Lock()
    defer c.mu.Unlock()

    c.data = make(map[string][]byte)
    return nil
}

// 使用例
func main() {
    var cache Cache = NewMemoryCache()

    cache.Set("user:1", []byte("Alice"))
    data, _ := cache.Get("user:1")
    fmt.Println(string(data))  // Alice

    cache.Delete("user:1")
    cache.Clear()
}

// モックやテストが容易
type MockCache struct {
    getCalls int
    setCalls int
}

func (m *MockCache) Get(key string) ([]byte, error) {
    m.getCalls++
    return []byte("mock"), nil
}

func (m *MockCache) Set(key string, value []byte) error {
    m.setCalls++
    return nil
}

// ... 他のメソッド

設計のポイント:

  • 小さいインターフェースに分割(単一責任原則)
  • 合成で大きいインターフェースを作成
  • 実装の交換が容易(インメモリ、Redis、ファイルなど)
  • テストとモックが容易

まとめ

この章では、Goのインターフェースをマシンレベルで深く理解しました。

🔑 重要ポイント:

  • 内部構造: インターフェースは2つのポインタ(tab/type + data)で構成される16バイトの構造体
  • 動的ディスパッチ: メソッド呼び出しは itab のメソッドテーブル経由で実行される
  • nil の落とし穴: nil ポインタを持つインターフェースは nil ではない
  • 型アサーション: ポインタ比較による高速な型チェック
  • 小さいインターフェース: 1-3メソッドのインターフェースが理想的
  • 暗黙的実装: 明示的な宣言不要で疎結合を実現

💡 パフォーマンスのヒント:

  • 必要な場所でのみインターフェースを使用
  • 型アサーションによる最適化パス
  • 空インターフェースの過度な使用を避ける
  • ジェネリクスの活用(Go 1.18+)

⚠️ よくある落とし穴:

  • nil インターフェースの誤解
  • []Type[]interface{} に直接変換できない
  • メソッドセット(値 vs ポインタレシーバー)の理解不足
  • インターフェースの不必要な使用

次の章では、エラー処理について、エラーインターフェースの実装とベストプラクティスを学びます。