Day 3: インターフェース - 解説
本日のまとめ
インターフェースの重要ポイント:
| 概念 | 説明 | 重要度 |
|---|---|---|
| 暗黙的実装 | 明示的な宣言不要 | ★★★ |
| 型アサーション | 安全な型変換 | ★★★ |
| 型スイッチ | 複数の型を扱う | ★★☆ |
| 空インターフェース | 任意の型を保持 | ★★☆ |
| デザインパターン | Strategy, Adapter, Decorator, DI | ★★★ |
| テスト | モックで依存を注入 | ★★★ |
---
なぜGoのインターフェースは特別なのか
暗黙的実装の威力
// 外部パッケージの型にインターフェースを適用できる
// 標準ライブラリの型も、自分で定義したインターフェースを満たす
type MyWriter interface {
Write([]byte) (int, error)
}
// os.File, bytes.Buffer, strings.Builder など
// すべてMyWriterを満たす(それらを修正せずに)
Duck Typing vs 暗黙的実装
# Python: Duck Typing(動的、実行時チェック)
def process(obj):
obj.write(b"hello") # 実行時にエラーの可能性
// Go: 暗黙的実装(静的、コンパイル時チェック)
func process(w io.Writer) {
w.Write([]byte("hello")) // コンパイル時にチェック
}
---
インターフェースの内部実装
インターフェース値の構造
// インターフェース値は2つのワードで構成される
type iface struct {
tab *itab // 型情報とメソッドテーブル
data unsafe.Pointer // 実際の値へのポインタ
}
nil インターフェースの注意点
func main() {
var w io.Writer
fmt.Println(w == nil) // true
var buf *bytes.Buffer = nil
w = buf
fmt.Println(w == nil) // false! (型情報がある)
// これは危険
if w != nil {
w.Write([]byte("hello")) // panic: buf is nil
}
}
---
型アサーションのパフォーマンス
// 型アサーションは高速(O(1))
if s, ok := i.(string); ok {
// ...
}
// 型スイッチも高速
switch v := i.(type) {
case string:
// ...
}
// リフレクションは遅い
reflect.TypeOf(i) // 型アサーションより10倍以上遅い
---
インターフェース設計のベストプラクティス
1. 消費者側でインターフェースを定義
// 悪い例:提供者側でインターフェースを定義
package storage
type Storage interface {
Get(key string) ([]byte, error)
Set(key string, value []byte) error
// 多くのメソッド...
}
// 良い例:消費者側で必要なものだけ定義
package userservice
type UserGetter interface {
Get(key string) ([]byte, error)
}
func GetUser(store UserGetter, id string) (*User, error) {
// ...
}
2. インターフェースを返さない
// 悪い例
func NewStorage() Storage {
return &MemoryStorage{}
}
// 良い例
func NewMemoryStorage() *MemoryStorage {
return &MemoryStorage{}
}
3. 最小限のインターフェース
// 悪い例
type Repository interface {
GetByID(id int) (*User, error)
GetByName(name string) (*User, error)
GetByEmail(email string) (*User, error)
Create(*User) error
Update(*User) error
Delete(id int) error
List() ([]*User, error)
Count() (int, error)
}
// 良い例
type UserGetter interface {
GetByID(id int) (*User, error)
}
type UserCreator interface {
Create(*User) error
}
// 必要な場所で組み合わせる
---
よくある間違い
1. インターフェースの乱用
// 悪い例:すべてにインターフェースを作る
type StringProcessor interface {
Process(s string) string
}
// 良い例:必要な時だけインターフェースを作る
func Process(s string) string {
return strings.ToUpper(s)
}
2. 空インターフェースの過度な使用
// 悪い例
func Store(data interface{}) {}
// 良い例:ジェネリクスを使う(Go 1.18+)
func Store[T any](data T) {}
// または具体的な型を使う
func StoreUser(user *User) {}
---
メンタルモデル:インターフェースの思考法
1. 「契約」として考える
インターフェースは実装の詳細ではなく、契約です。
// 契約: 「私は書き込むことができます」
type Writer interface {
Write([]byte) (int, error)
}
// 契約を守る実装
type FileWriter struct { /* ... */ }
func (w *FileWriter) Write(p []byte) (int, error) { /* ... */ }
type NetworkWriter struct { /* ... */ }
func (w *NetworkWriter) Write(p []byte) (int, error) { /* ... */ }
// 契約を信頼するコード
func SaveData(w Writer, data []byte) error {
// Writerの具体的な実装は知らない、契約だけを信頼
_, err := w.Write(data)
return err
}
2. 「振る舞い」で考える
型ではなく、何ができるかで考えます。
// 悪い思考: 「Userオブジェクトを保存する」
type UserRepository struct {
// User固有の実装...
}
// 良い思考: 「エンティティを保存できる」
type Repository[T any] interface {
Save(entity T) error
Find(id string) (T, error)
}
// 任意の型で使える
var userRepo Repository[User]
var productRepo Repository[Product]
3. 「構成」で考える
継承ではなく、構成で機能を組み立てます。
// 小さなインターフェースを定義
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 ReadWriteCloser interface {
Reader
Writer
Closer
}
// 必要な機能だけを要求
func ProcessFile(r Reader) error {
// ReadだけできればOK
}
---
設計原則:SOLID再訪(インターフェース編)
Interface Segregation Principle の深掘り
クライアントは使わないメソッドに依存すべきではありません。
// 現実的な例: ドキュメント管理システム
// 悪い設計:全機能を1つのインターフェースに
type DocumentManager interface {
Create(doc *Document) error
Read(id string) (*Document, error)
Update(doc *Document) error
Delete(id string) error
Search(query string) ([]*Document, error)
Index(doc *Document) error
Backup() error
Restore(backup *Backup) error
Audit(action string) error
}
// 問題:
// - 読み取り専用クライアントも全メソッドを知る必要がある
// - モックが複雑になる
// - 変更の影響範囲が大きい
// 良い設計: 役割で分離
type DocumentReader interface {
Read(id string) (*Document, error)
Search(query string) ([]*Document, error)
}
type DocumentWriter interface {
Create(doc *Document) error
Update(doc *Document) error
Delete(id string) error
}
type DocumentIndexer interface {
Index(doc *Document) error
}
type DocumentBackup interface {
Backup() error
Restore(backup *Backup) error
}
// クライアントは必要な機能だけを要求
type ReportGenerator struct {
reader DocumentReader // 読み取りだけ必要
}
type AdminPanel struct {
reader DocumentReader
writer DocumentWriter
backup DocumentBackup
}
Dependency Inversion の実践例
// レイヤードアーキテクチャでの DIP
// プレゼンテーション層
package handler
// ビジネスロジックへの依存(インターフェース)
type UserService interface {
GetUser(ctx context.Context, id string) (*User, error)
CreateUser(ctx context.Context, user *User) error
}
type UserHandler struct {
service UserService // 抽象に依存
}
// ビジネスロジック層
package service
// データアクセスへの依存(インターフェース)
type UserRepository interface {
FindByID(ctx context.Context, id string) (*User, error)
Save(ctx context.Context, user *User) error
}
type userService struct {
repo UserRepository // 抽象に依存
}
// データアクセス層
package repository
type postgresUserRepository struct {
db *sql.DB // 具象型(外部ライブラリ)
}
// 依存関係の流れ:
// Handler -> Service (interface) <- serviceImpl
// ↓
// Repository (interface) <- repositoryImpl
//
// すべてのレイヤーが抽象(インターフェース)に依存
---
さらなる探求:高度なインターフェーステクニック
1. 型パラメータ付きインターフェース(Go 1.18+)
// ジェネリックなリポジトリインターフェース
type Repository[T any] interface {
FindByID(ctx context.Context, id string) (T, error)
Save(ctx context.Context, entity T) error
Delete(ctx context.Context, id string) error
List(ctx context.Context) ([]T, error)
}
// 具体的な型で使用
type UserRepository Repository[User]
type ProductRepository Repository[Product]
// 実装
type InMemoryRepository[T any] struct {
items map[string]T
}
func (r *InMemoryRepository[T]) FindByID(ctx context.Context, id string) (T, error) {
item, ok := r.items[id]
if !ok {
var zero T
return zero, errors.New("not found")
}
return item, nil
}
// 使用例
userRepo := &InMemoryRepository[User]{items: make(map[string]User)}
productRepo := &InMemoryRepository[Product]{items: make(map[string]Product)}
2. インターフェースの制約(Constraints)
// 数値型の制約
type Number interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 |
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 |
~float32 | ~float64
}
// 比較可能な型の制約
type Comparable interface {
comparable
}
// メソッドを持つ制約
type Stringer interface {
String() string
}
// 組み合わせ
func Max[T Number](a, b T) T {
if a > b {
return a
}
return b
}
func PrintAll[T Stringer](items []T) {
for _, item := range items {
fmt.Println(item.String())
}
}
3. インターフェースのエンベッディング
// 基本インターフェース
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
// エンベッディングで組み合わせ
type ReadWriter interface {
Reader // Readメソッドを持つ
Writer // Writeメソッドを持つ
}
// メソッドのオーバーライドはできない(これはエラー)
type BrokenInterface interface {
Reader
Read(p []byte) (n int, err error) // エラー: 重複定義
}
// 実装
type Buffer struct {
data []byte
}
func (b *Buffer) Read(p []byte) (int, error) {
// 実装
}
func (b *Buffer) Write(p []byte) (int, error) {
// 実装
}
// BufferはReadWriterを自動的に実装
var _ ReadWriter = (*Buffer)(nil)
4. インターフェースのnil問題の深掘り
package main
import "fmt"
type MyError struct {
msg string
}
func (e *MyError) Error() string {
if e == nil {
return "no error"
}
return e.msg
}
// ケース1: nil具象型を返す
func returnNilConcrete() *MyError {
return nil // *MyError型のnil
}
// ケース2: nilインターフェースを返す
func returnNilInterface() error {
return nil // error型のnil
}
// ケース3: nil具象型をインターフェースに変換して返す
func returnConvertedNil() error {
var err *MyError = nil
return err // 危険!
}
func main() {
// ケース1: nil具象型
err1 := returnNilConcrete()
fmt.Printf("err1 == nil: %v\n", err1 == nil) // true
// ケース2: nilインターフェース
err2 := returnNilInterface()
fmt.Printf("err2 == nil: %v\n", err2 == nil) // true
// ケース3: 変換されたnil(落とし穴!)
err3 := returnConvertedNil()
fmt.Printf("err3 == nil: %v\n", err3 == nil) // false!
// err3は型情報を持つため、nilではない
// しかし、err3.Error()を呼ぶとpanicする可能性がある
}
// 正しいパターン
func correctPattern() error {
var err *MyError = nil
if err != nil {
return err // errがnilでない場合のみ返す
}
return nil // 明示的にnilを返す
}
---
ビジュアル図解:インターフェースの理解
インターフェース値のメモリ構造
インターフェース値 (16 bytes on 64-bit)
┌─────────────────────────────┐
│ Type Pointer (8 bytes) │ → 型情報(itab)
├─────────────────────────────┤
│ Data Pointer (8 bytes) │ → 実際の値
└─────────────────────────────┘
例1: var w io.Writer = &FileWriter{path: "test.txt"}
┌─────────────────────────────┐
│ *itab (FileWriter → Writer)│ → メソッドテーブル
├─────────────────────────────┤
│ *FileWriter │ → FileWriterインスタンス
└─────────────────────────────┘
例2: var w io.Writer = nil
┌─────────────────────────────┐
│ nil (0x0) │
├─────────────────────────────┤
│ nil (0x0) │
└─────────────────────────────┘
例3: var e error = (*MyError)(nil) // 危険!
┌─────────────────────────────┐
│ *itab (MyError → error) │ → 型情報あり!
├─────────────────────────────┤
│ nil (0x0) │ → データはnil
└─────────────────────────────┘
この場合、e == nil は false になる!
型アサーションのフロー
var i interface{} = "hello"
s, ok := i.(string)
ステップ1: 型情報を取得
i の Type Pointer を確認
ステップ2: 型を比較
Type Pointer が string を指しているか?
Yes → ステップ3へ
No → ok = false, s = ""(ゼロ値)
ステップ3: データを取得
Data Pointer から値をコピー
s = "hello", ok = true
パフォーマンス: O(1) (ハッシュテーブルルックアップ)
型スイッチの評価順序
func process(i interface{}) {
switch v := i.(type) {
case int: // ケース1
// ...
case string: // ケース2
// ...
case io.Reader: // ケース3
// ...
case interface{ Close() error }: // ケース4
// ...
default: // ケース5
// ...
}
}
評価フロー:
1. i の型を取得
2. 各caseを順番に比較
3. 最初にマッチしたcaseを実行
4. どれもマッチしなければdefault
注意: ケースの順序が重要!
より具体的な型を先に書く
---
プロダクション事例:実際のアンチパターンとその修正
実例1: エラーインターフェースの誤用
// 悪いコード(実際のプロジェクトから)
type ValidationError struct {
Errors []string
}
func (e *ValidationError) Error() string {
return strings.Join(e.Errors, ", ")
}
func ValidateUser(user *User) error {
var err ValidationError
if user.Name == "" {
err.Errors = append(err.Errors, "name is required")
}
if user.Email == "" {
err.Errors = append(err.Errors, "email is required")
}
// 落とし穴!
return &err // errが空でもnilではない
}
func main() {
err := ValidateUser(&User{Name: "Alice", Email: "alice@example.com"})
if err != nil { // true になる!
fmt.Println("Validation failed:", err)
}
}
// 修正版
func ValidateUser(user *User) error {
var errors []string
if user.Name == "" {
errors = append(errors, "name is required")
}
if user.Email == "" {
errors = append(errors, "email is required")
}
if len(errors) > 0 {
return &ValidationError{Errors: errors} // エラーがある場合のみ返す
}
return nil // 明示的にnilを返す
}
実例2: インターフェースのレイヤー汚染
// 悪いコード
package repository
// インターフェースを提供者側で定義
type UserRepository interface {
GetUser(id string) (*User, error)
SaveUser(user *User) error
}
package service
import "myapp/repository"
type UserService struct {
repo repository.UserRepository // repository に依存
}
// 問題:
// - serviceパッケージがrepositoryパッケージに依存
// - repositoryの変更がserviceに影響
// - テストが困難
// 修正版
package service
// サービス層でインターフェースを定義
type UserRepository interface {
GetUser(id string) (*User, error)
SaveUser(user *User) error
}
type UserService struct {
repo UserRepository // 自分のインターフェースに依存
}
package repository
import "myapp/service"
type PostgresUserRepository struct {
db *sql.DB
}
// service.UserRepository を実装(暗黙的)
func (r *PostgresUserRepository) GetUser(id string) (*User, error) { /* ... */ }
func (r *PostgresUserRepository) SaveUser(user *User) error { /* ... */ }
// メリット:
// - 依存関係が逆転(DIP)
// - repository の変更が service に影響しない
// - モックテストが容易
---
明日の予習
Day 4: 並行処理の基礎では、以下を学びます:
- ゴルーチンの基本とCSPモデル
- sync.WaitGroupによる同期
- sync.Mutex / RWMutexによる排他制御
- atomic操作と無ロック並行処理
- 並行処理のパターンとアンチパターン
- データ競合の検出とデバッグ
予習課題
以下のコードに潜むバグを見つけてください:
package main
import (
"fmt"
"sync"
)
type Counter struct {
value int
}
func (c *Counter) Increment() {
c.value++
}
func (c *Counter) Value() int {
return c.value
}
func main() {
counter := &Counter{}
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
counter.Increment()
}()
}
wg.Wait()
fmt.Println("Final count:", counter.Value())
}
質問:
- このコードを実行すると、
Final countは常に 1000 になるでしょうか? - もしならない場合、その理由は何でしょうか?
- どのように修正すればよいでしょうか?
---
参考リソース
必読ドキュメント
- Go Blog: The Laws of Reflection
- Effective Go: Interfaces
- Go Wiki: Interface Implementation
- Go Data Structures: Interfaces
推奨記事
- "How to use interfaces in Go" by Jordan Orelli
- "Accept Interfaces, Return Structs" by Jack Lindamood
- "Go Interfaces Are Beautiful" by Dave Cheney
オープンソースプロジェクトの事例
- io.Reader / io.Writer: 標準ライブラリの最高の例
- database/sql.Driver: プラグイン可能なドライバーシステム
- net/http.Handler: ミドルウェアパターンの典型
---
インターフェースはGoの最も強力な機能です。小さく保ち、組み合わせて使うことで、柔軟で保守性の高いコードが書けます!
Remember: "The bigger the interface, the weaker the abstraction." - Rob Pike