インターフェース

レッスン9:インターフェース

現実世界のアナロジー

レストランで料理を注文する場面を想像してください。シェフが誰か、どんなフライパンを使っているか、どのように調理しているかを知る必要はありません — メニューを見て「エビチリ」を注文するだけです。メニューはインターフェースです。「何ができるか」を定義し、「誰がやるか」「どうやるか」は気にしません。

Goのインターフェースも同じように動作します。メソッドシグネチャの集合を定義し、これらのメソッドを実装するすべての型が自動的にインターフェースを満たします — 明示的な宣言は不要です。


コアコンセプト

コンセプト 説明
インターフェース メソッドシグネチャの集合で、振る舞いの契約を定義
暗黙の実装 型がインターフェースのすべてのメソッドを実装していれば、自動的にインターフェースを満たす
ダックタイピング 「アヒルのように歩き、アヒルのように鳴くなら、それはアヒル」
空のインターフェース interface{} メソッドを含まない。すべての型がこれを満たす
型アサーション インターフェース値から具象型を抽出する
インターフェースの合成 複数のインターフェースを埋め込んで大きなインターフェースを構築

基本構文と使い方

インターフェースの定義

GO
// Speaker インターフェースを定義
type Speaker interface {
    Speak() string
}

暗黙の実装

GO
// Dog 型は Speaker インターフェースを実装(宣言は不要)
type Dog struct {
    Name string
}

func (d Dog) Speak() string {
    return "ワン!私は" + d.Name + "です"
}

// Cat 型も Speaker インターフェースを実装
type Cat struct {
    Name string
}

func (c Cat) Speak() string {
    return "ニャー!私は" + c.Name + "です"
}
💡 ヒント: Goには implements キーワードがありません。型がインターフェースが必要とするすべてのメソッドを持っていれば、自動的にそのインターフェースを実装します。

インターフェースの使用

GO
func makeItSpeak(s Speaker) {
    fmt.Println(s.Speak())
}

func main() {
    dog := Dog{Name: "Rex"}
    cat := Cat{Name: "Whiskers"}

    makeItSpeak(dog) // 出力: ワン!私はRexです
    makeItSpeak(cat) // 出力: ニャー!私はWhiskersです
}

空のインターフェース interface{}

GO
// 空のインターフェースは任意の型の値を保持できる
func printAnything(v interface{}) {
    fmt.Printf("値: %v、型: %T\n", v, v)
}

func main() {
    printAnything(42)         // 値: 42、型: int
    printAnything("hello")   // 値: hello、型: string
    printAnything(3.14)      // 値: 3.14、型: float64
}
💡 ヒント: Go 1.18以降、interface{}any に短縮できます。両者は等価です。

型アサーションと型スイッチ

GO
func describe(v interface{}) {
    // 型アサーション:インターフェース値を具象型に変換を試みる
    str, ok := v.(string)
    if ok {
        fmt.Println("これは文字列です:", str)
        return
    }

    // 型スイッチ:複数の型を优雅に処理
    switch val := v.(type) {
    case int:
        fmt.Println("これは整数です:", val)
    case float64:
        fmt.Println("これは浮動小数点数です:", val)
    case bool:
        fmt.Println("これはブール値です:", val)
    default:
        fmt.Printf("不明な型: %T\n", val)
    }
}
💡 ヒント: 型アサーションで「comma ok」パターンを使用するとpanicを回避できます。v.(Type) はアサーションが失敗するとpanicを起こしますが、v, ok := v.(Type) は安全にゼロ値と false を返します。

インターフェースの合成

GO
// ベースインターフェース
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
}

// ReadWriter は Read と Write の両方のメソッドの実装が必要
💡 ヒント: インターフェースの合成は「小さなインターフェース」の原則に従います。Goの標準ライブラリの多くのインターフェースは1〜2つのメソッドしか持っていません。例えば、io.Readerio.Writerfmt.Stringer などです。


サンプルコード

例:図形の面積計算(難易度⭐)

GO
package main

import (
    "fmt"
    "math"
)

// Shape インターフェースは「図形」の振る舞いを定義
type Shape interface {
    Area() float64
    Perimeter() float64
}

// Rectangle(長方形)
type Rectangle struct {
    Width  float64
    Height float64
}

func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

func (r Rectangle) Perimeter() float64 {
    return 2 * (r.Width + r.Height)
}

// Circle(円)
type Circle struct {
    Radius float64
}

func (c Circle) Area() float64 {
    return math.Pi * c.Radius * c.Radius
}

func (c Circle) Perimeter() float64 {
    return 2 * math.Pi * c.Radius
}

// printShapeInfo は Shape インターフェースの任意の実装を受け取る
func printShapeInfo(s Shape) {
    fmt.Printf("面積: %.2f、周囲の長さ: %.2f\n", s.Area(), s.Perimeter())
}

func main() {
    rect := Rectangle{Width: 10, Height: 5}
    circle := Circle{Radius: 7}

    fmt.Print("長方形 -> ")
    printShapeInfo(rect)

    fmt.Print("円 -> ")
    printShapeInfo(circle)
}
▶ 試してみよう
長方形 -> 面積: 50.00、周囲の長さ: 30.00
円 -> 面積: 153.94、周囲の長さ: 43.98

例:インターフェースのスライスとソート(難易度⭐⭐)

GO
package main

import (
    "fmt"
    "sort"
)

// Employee インターフェース
type Employee interface {
    Name() string
    Salary() float64
}

// FullTime は正社員
type FullTime struct {
    name   string
    annual float64 // 年間給与
}

func (f FullTime) Name() string    { return f.name }
func (f FullTime) Salary() float64 { return f.annual }

// Contractor は契約社員
type Contractor struct {
    name    string
    hourly  float64 // 時給
    hours   float64 // 労働時間
}

func (c Contractor) Name() string    { return c.name }
func (c Contractor) Salary() float64 { return c.hourly * c.hours }

// BySalary は sort.Interface を実装し、給与でソートする
type BySalary []Employee

func (s BySalary) Len() int           { return len(s) }
func (s BySalary) Less(i, j int) bool { return s[i].Salary() < s[j].Salary() }
func (s BySalary) Swap(i, j int)      { s[i], s[j] = s[j], s[i] }

// totalCost は総人件費を計算する
func totalCost(employees []Employee) float64 {
    total := 0.0
    for _, e := range employees {
        total += e.Salary()
    }
    return total
}

func main() {
    team := []Employee{
        FullTime{name: "Zhang San", annual: 120000},
        Contractor{name: "Li Si", hourly: 200, hours: 1000},
        FullTime{name: "Wang Wu", annual: 150000},
        Contractor{name: "Zhao Liu", hourly: 180, hours: 800},
    }

    fmt.Println("=== 給与ソート前 ===")
    for _, e := range team {
        fmt.Printf("  %s: $%.0f\n", e.Name(), e.Salary())
    }

    sort.Sort(BySalary(team))

    fmt.Println("\n=== 給与ソート後 ===")
    for _, e := range team {
        fmt.Printf("  %s: $%.0f\n", e.Name(), e.Salary())
    }

    fmt.Printf("\n総人件費: $%.0f\n", totalCost(team))
}
▶ 試してみよう
=== 給与ソート前 ===
  Zhang San: $120000
  Li Si: $200000
  Wang Wu: $150000
  Zhao Liu: $144000

=== 給与ソート後 ===
  Zhang San: $120000
  Zhao Liu: $144000
  Wang Wu: $150000
  Li Si: $200000

総人件費: $614000

例:io.Reader/Writerインターフェースの実装(難易度⭐⭐⭐)

GO
package main

import (
    "fmt"
    "io"
    "strings"
)

// UpperReader は読み込んだ内容を大文字に変換する
type UpperReader struct {
    source io.Reader
}

// io.Reader インターフェースを実装
func (u *UpperReader) Read(p []byte) (n int, err error) {
    n, err = u.source.Read(p)
    // 読み込んだすべてのバイトを大文字に変換
    for i := 0; i < n; i++ {
        if p[i] >= 'a' && p[i] <= 'z' {
            p[i] = p[i] - 32 // ASCII: 小文字を大文字に
        }
    }
    return
}

// UpperReader のコンストラクタ
func NewUpperReader(r io.Reader) *UpperReader {
    return &UpperReader{source: r}
}

// PrefixWriter は各書き込みの前にプレフィックスを追加する
type PrefixWriter struct {
    prefix string
    target io.Writer
}

// io.Writer インターフェースを実装
func (p *PrefixWriter) Write(data []byte) (n int, err error) {
    // まずプレフィックスを書き込む
    _, err = p.target.Write([]byte(p.prefix))
    if err != nil {
        return 0, err
    }
    // 次に実際のデータを書き込む
    return p.target.Write(data)
}

// PrefixWriter のコンストラクタ
func NewPrefixWriter(prefix string, w io.Writer) *PrefixWriter {
    return &PrefixWriter{prefix: prefix, target: w}
}

// TeeReader は読み込みと同時に書き込む(teeコマンドに類似)
func TeeReader(r io.Reader, w io.Writer) io.Reader {
    return &teeReader{r: r, w: w}
}

type teeReader struct {
    r io.Reader
    w io.Writer
}

func (t *teeReader) Read(p []byte) (n int, err error) {
    n, err = t.r.Read(p)
    if n > 0 {
        // 読み込みながら w にも書き込む
        t.w.Write(p[:n])
    }
    return
}

func main() {
    fmt.Println("=== UpperReader の例 ===")
    // 文字列から Reader を作成
    source := strings.NewReader("hello, go interfaces!")
    upper := NewUpperReader(source)

    // io.ReadAll ですべてのコンテンツを読み込む
    buf := make([]byte, 64)
    n, _ := upper.Read(buf)
    fmt.Printf("大文字変換結果: %s\n", string(buf[:n]))

    fmt.Println("\n=== PrefixWriter の例 ===")
    // プレフィックス付きでstdoutに書き込む
    writer := NewPrefixWriter("[LOG] ", &strings.Builder{})
    writer.Write([]byte("システムが起動しました\n"))
    // strings.Builder を使って出力をキャプチャ
    var builder strings.Builder
    pw := NewPrefixWriter("[DEBUG] ", &builder)
    pw.Write([]byte("インターフェースが初期化されました"))
    fmt.Println(builder.String())

    fmt.Println("\n=== TeeReader の例 ===")
    // 読み込みながら別のWriterにも書き込む
    input := strings.NewReader("Go is powerful")
    var capture strings.Builder
    tee := TeeReader(input, &capture)

    buf2 := make([]byte, 1024)
    n2, _ := tee.Read(buf2)
    fmt.Printf("読み込み: %s\n", string(buf2[:n2]))
    fmt.Printf("キャプチャされた内容: %s\n", capture.String())
}
▶ 試してみよう
=== UpperReader の例 ===
大文字変換結果: HELLO, GO INTERFACES!

=== PrefixWriter の例 ===
[DEBUG] インターフェースが初期化されました

=== TeeReader の例 ===
読み込み: Go is powerful
キャプチャされた内容: Go is powerful

現実世界の使用場面

場面1:ログシステム(ストラテジーパターン)

GO
package main

import (
    "fmt"
    "os"
    "time"
)

// Logger はログ出力インターフェース
type Logger interface {
    Log(message string)
}

// ConsoleLogger はコンソールログ出力
type ConsoleLogger struct{}

func (c ConsoleLogger) Log(message string) {
    timestamp := time.Now().Format("2006-01-02 15:04:05")
    fmt.Printf("[%s] %s\n", timestamp, message)
}

// FileLogger はファイルログ出力
type FileLogger struct {
    file *os.File
}

func NewFileLogger(filename string) (*FileLogger, error) {
    f, err := os.OpenFile(filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
    if err != nil {
        return nil, err
    }
    return &FileLogger{file: f}, nil
}

func (f *FileLogger) Log(message string) {
    timestamp := time.Now().Format("2006-01-02 15:04:05")
    fmt.Fprintf(f.file, "[%s] %s\n", timestamp, message)
}

// MultiLogger は複数のロガーに同時出力する
type MultiLogger struct {
    loggers []Logger
}

func (m *MultiLogger) Add(l Logger) {
    m.loggers = append(m.loggers, l)
}

func (m *MultiLogger) Log(message string) {
    for _, l := range m.loggers {
        l.Log(message)
    }
}

// App は Logger インターフェースを使用し、具体的な実装を気にしない
type App struct {
    logger Logger
}

func (a *App) Run() {
    a.logger.Log("アプリケーションが起動しました")
    a.logger.Log("リクエストを処理中...")
    a.logger.Log("リクエスト処理が完了しました")
}

func main() {
    // 複数のログ出力を組み合わせ
    multi := &MultiLogger{}
    multi.Add(ConsoleLogger{})

    // ログ出力方法を簡単に切り替えたり追加したりできる
    app := &App{logger: multi}
    app.Run()
}
[2026-06-26 10:30:00] アプリケーションが起動しました
[2026-06-26 10:30:00] リクエストを処理中...
[2026-06-26 10:30:00] リクエスト処理が完了しました

場面2:データストレージの抽象化レイヤー

GO
package main

import "fmt"

// Store はストレージインターフェース
type Store interface {
    Get(key string) (string, bool)
    Set(key string, value string)
    Delete(key string)
    Keys() []string
}

// MemoryStore はインメモリストレージの実装
type MemoryStore struct {
    data map[string]string
}

func NewMemoryStore() *MemoryStore {
    return &MemoryStore{data: make(map[string]string)}
}

func (m *MemoryStore) Get(key string) (string, bool) {
    val, ok := m.data[key]
    return val, ok
}

func (m *MemoryStore) Set(key string, value string) {
    m.data[key] = value
}

func (m *MemoryStore) Delete(key string) {
    delete(m.data, key)
}

func (m *MemoryStore) Keys() []string {
    keys := make([]string, 0, len(m.data))
    for k := range m.data {
        keys = append(keys, k)
    }
    return keys
}

// CacheService は Store インターフェースを使用し、具体的なストレージから分離されている
type CacheService struct {
    store Store
}

func (c *CacheService) GetOrSet(key, defaultValue string) string {
    if val, ok := c.store.Get(key); ok {
        return val
    }
    c.store.Set(key, defaultValue)
    return defaultValue
}

func (c *CacheService) GetAll() map[string]string {
    result := make(map[string]string)
    for _, key := range c.store.Keys() {
        if val, ok := c.store.Get(key); ok {
            result[key] = val
        }
    }
    return result
}

func main() {
    // インメモリストレージを使用
    store := NewMemoryStore()
    cache := &CacheService{store: store}

    // データを書き込む
    cache.GetOrSet("user:1", "Alice")
    cache.GetOrSet("user:2", "Bob")
    cache.GetOrSet("config:theme", "dark")

    // データを読み込む
    fmt.Println("すべてのキャッシュデータ:")
    for k, v := range cache.GetAll() {
        fmt.Printf("  %s = %s\n", k, v)
    }

    // GetOrSet のテスト:既存のキーは古い値を返す
    result := cache.GetOrSet("user:1", "Charlie")
    fmt.Printf("\nuser:1 の値: %s\n", result)
}
すべてのキャッシュデータ:
  user:1 = Alice
  user:2 = Bob
  config:theme = dark

user:1 の値: Alice

📖 まとめ

重要ポイント 説明
インターフェースは振る舞いを定義 「何ができるか」だけを気にし、「何であるか」は気にしない
暗黙の実装 宣言は不要。メソッドを実装すればインターフェースを満たす
空のインターフェース any 任意の型の値を保持できる
型アサーション インターフェース値から具象型を抽出。安全のため comma ok パターンを使用
インターフェースの合成 小さなインターフェースを埋め込んで大きなインターフェースを構築
インターフェースに基づいてプログラミング 具象実装ではなくインターフェースに依存することで柔軟性を実現
💡 ベストプラクティス: Goは小さなインターフェースを好みます。標準ライブラリで最もよく使われるインターフェースは通常1〜2つのメソッドしか持っていません。インターフェースを定義するときは、実装者の視点ではなく、利用者(呼び出し側)のニーズから始めてください。


❓ よくある質問

質問1:インターフェースと構造体の違いは?

構造体は「何であるか」を定義する具象データ型です。インターフェースは「何ができるか」を定義する振る舞いの契約です。構造体はインスタンス化できますが、インターフェースは直接インスタンス化することはできず、インターフェースを満たす任意の型の値を保持できます。

質問2:なぜGoには implements キーワードがいらないの?

Goはダックタイピングの設計を採用しています。コンパイラはコンパイル時に型がインターフェースを満たすかどうかを自動的にチェックします。この設計により、インターフェースと実装は完全に分離されます — 既存のコードを変更せずに、サードパーティライブラリの型に新しいインターフェースを定義できます。

質問3:いつインターフェースを定義すべき?

💡 目安: まず具象実装を書き、抽象化の必要性が出てきたらインターフェースを定義してください。過度な設計は避けてください。

質問4:interface{}any の違いは?

ありません。any はGo 1.18で導入された interface{} の型エイリアスです。完全に等価です。簡潔なので any が推奨されます。


📝 演習

演習1:基礎 — Stringerインターフェースの実装

fmt.Stringer インターフェースは String() string メソッドのみを持ちます。fmt.Println%v フォーマットを使用する際、Goが自動的にこのメソッドを呼び出します。

GO
// 以下の型に対して fmt.Stringer インターフェースを実装してください
type Temperature struct {
    Celsius float64
}

type Money struct {
    Amount   float64
    Currency string
}

// 期待される動作:
// fmt.Println(Temperature{36.5})  -> "36.5°C"
// fmt.Println(Money{99.9, "USD"}) -> "$99.90"

演習2:中級 — 通知システムの設計

複数の通知方法をサポートする通知システムを設計してください:

GO
// 1. Notifier インターフェースを定義
// 2. EmailNotifier、SMSNotifier、WechatNotifier を実装
// 3. 複数の Notifier に同時に送信できる関数を実装
// 4. インターフェースのスライスを使って異なる Notifier を格納

演習3:チャレンジ — 簡易プラグインシステムの実装

GO
// Name()、Version()、Execute() メソッドを持つ Plugin インターフェースを定義
// 少なくとも3つの異なる Plugin を実装
// プラグインの登録、検索、実行ができる PluginManager を作成
// ヒント:map[string]Plugin を使ってプラグインを格納

次のレッスン

インターフェースはGoにおける多態性の基盤です。インターフェースをマスターすれば、柔軟でテスト可能、拡張可能なコードを書くことができます。次は、Goのエラー処理メカニズムについて学びます — Go言語で最も重要な設計哲学の一つです。

👉 次のレッスン:エラー処理

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%