第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.Println や fmt.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は trueerr2: 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)
説明:
- この行は実行時には何もしないが、コンパイル時に型チェックが行われる
Bufferがio.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: コンパイル成功
CounterはGetValue()メソッドを持つ(値レシーバー)- 値レシーバーのメソッドは、値と型両方のメソッドセットに含まれる
ケース2: コンパイルエラー
CounterはIncrement()メソッドを持たない(*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 の方が高速です。
理由:
- 静的ディスパッチ:
sum2はbytes.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 ポインタレシーバー)の理解不足
- インターフェースの不必要な使用
次の章では、エラー処理について、エラーインターフェースの実装とベストプラクティスを学びます。