Go中級 #1 インターフェース — 暗黙的実装の意味

Go中級シリーズの最初の記事です。基礎7編を終えていれば小さなツールは自信を持って作れるはずですが、中級はその上に — Goの本当の強みを載せる段階です。

全7編で構成されます。

  • #1 インターフェース ← 今回の記事
  • #2 エラー処理パターン
  • #3 ゴルーチンとチャネル入門
  • #4 selectとタイムアウト
  • #5 context.Contextの深掘り
  • #6 テスティング
  • #7 標準ライブラリツアー

今回の記事は — Goの最も際立つ設計判断のひとつ、インターフェース

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

基本インターフェース
type Speaker interface {
	Speak() string
}

type 名前 interface { メソッドたち } — この形自体は他の言語と似ています。違いはどう実装するかにあります。

暗黙的実装 — Goのシグネチャ #

Goのインターフェースには、implementsキーワードがありません

暗黙的実装
type Speaker interface {
	Speak() string
}

type Dog struct{}

func (d Dog) Speak() string {
	return "ワンワン"
}

func main() {
	var s Speaker = Dog{}    // 自動で実装される
	fmt.Println(s.Speak())
}

DogSpeak() stringメソッドを持っていれば — 自動的にSpeakerインターフェースを満たします。どこにも「Dog implements Speaker」のような宣言はありません。

これを構造的型付け(structural typing)またはduck typingのコンパイル時版と呼びます。TypeScriptのインターフェースも同じ方式です。Javaのような明示的インターフェースシステムとは正反対。

なぜこの設計なのか? #

この設計の大きな利点:

  1. インターフェースを使う側で定義できる — ライブラリがインターフェースを公開しなくても、使う側で必要なメソッドセットをインターフェースとして定義し受け取れます。
  2. 疎結合 — ライブラリAの型とライブラリBの型が同じインターフェースを満たせる(互いに知らない関係でも)
  3. 段階的抽象化 — 最初は具体型で書き、後でインターフェースに抽出できる

インターフェースの核心 — 使う側で定義 #

伝統的なOOPが「ライブラリがインターフェースを定義し、実装側がimplementsする」だとすれば、Goは「ユーザーが自分に必要なインターフェースを定義する」が自然です。

使う側でインターフェース定義
package mylogger

// 私たちのロガーが必要とするもの — Writerインターフェース
type Writer interface {
	Write(p []byte) (n int, err error)
}

func WriteLog(w Writer, msg string) {
	w.Write([]byte(msg))
}
呼び出し
import (
	"os"
	"bytes"
	"mylogger"
)

func main() {
	mylogger.WriteLog(os.Stdout, "stdout に")        // os.File が Writer を満たす
	mylogger.WriteLog(&bytes.Buffer{}, "buffer に")   // bytes.Buffer も Writer を満たす
}

os.Filebytes.Buffermylogger.Writerインターフェースを事前に知ることはできません。私たちが自分のコードに必要な形をインターフェースとして定義し、両方の型がたまたまその形を満たすので自動的に互換性があります。

標準ライブラリの中核インターフェース — io.Readerio.Writer #

最も頻繁に使われるインターフェース2つ。GoのI/Oシステム全体がこの上に立っています。

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は両方を満たす(ファイル読み書き)
  • net.Connは両方を満たす(ネットワーク送受信)
  • bytes.Bufferは両方を満たす(メモリバッファ)
  • strings.ReaderはReaderを満たす
  • gzip.WriterはWriterを満たす(圧縮 + 別のWriterへ転送)

同じ関数を — ファイルでもネットワークソケットでもメモリバッファでも同じように扱えます。非常に強力な抽象化です。

同じ関数、多様な入力源
func processData(r io.Reader) error {
	// ...
}

processData(file)         // ファイル
processData(httpResponse.Body)   // ネットワーク
processData(strings.NewReader("hello"))   // 文字列
processData(&bytes.Buffer{})    // メモリ

小さなインターフェースのガイド #

Goコミュニティの強い慣習:

インターフェースは小さいほど良い。メソッド1つか2つが理想。

このガイドの理由 — 小さいインターフェースほどより多くの型に自動で満たされます。メソッドが多くなるほどその形を満たす型が減り、活用度が下がります。

io.Readerio.Writererrorfmt.Stringerはすべてメソッド1つです。大きなインターフェースが必要なら複数の小さなインターフェースの合成で作ってください。

インターフェース合成
type ReadWriter interface {
	Reader
	Writer
}

io.ReadWriterがまさにこの定義です。

空インターフェース — interface{} (= any) #

メソッドが0個のインターフェースは、すべての型が満たします。

空インターフェース
type Empty interface{}      // 古い表記

// Go 1.18+ から any 別名が標準
var x any = 42
var y any = "hello"
var z any = []int{1, 2, 3}

anyinterface{}の別名です(Go 1.18+)。新しいコードはほぼすべてanyを使います。

JavaScriptのany、TypeScriptのunknownの位置に近いです。すべての型を受け取りますが — 使う前に絞り込む必要があります

型アサーション (Type Assertion) #

anyが中にどんな型を持っているかを取り出すには — 型アサーション

アサーション — シンプル
var v any = "hello"

s := v.(string)
fmt.Println(s)         // hello

n := v.(int)            // ✗ panic: interface conversion: ...

型が合えばOK、違えばpanic。危険です。

安全なアサーション — comma-ok #

comma-ok アサーション
var v any = "hello"

if s, ok := v.(string); ok {
	fmt.Println("string:", s)
} else {
	fmt.Println("string ではない")
}

2番目の値okがfalseなら間違った型。panicは起きません。ほぼ常にこの形が安全です。

型switch — 複数の型で分岐 #

anyが複数の型のうちのいずれかであるとき。

型 switch
func describe(i any) {
	switch v := i.(type) {
	case int:
		fmt.Printf("int: %d\n", v)
	case string:
		fmt.Printf("string: %s\n", v)
	case bool:
		fmt.Printf("bool: %t\n", v)
	case []int:
		fmt.Printf("[]int: %v\n", v)
	default:
		fmt.Printf("unknown type %T\n", v)
	}
}

switch v := i.(type)という特殊な構文。各case内でvがその型に絞り込まれます。JSのtypeof switchに近い位置づけです。

インターフェース変数のnilの罠 #

最も紛らわしい部分です。

nil の罠
type MyError struct{ msg string }

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

func doSomething() error {
	var err *MyError = nil
	return err   // *MyError が nil のまま error インターフェースに格納
}

func main() {
	err := doSomething()
	if err != nil {
		fmt.Println("エラーあり")   // ✗ 出力される!
	}
}

err != nilがtrueになります。なぜなら — インターフェース値は(型、値)のペアなので、中の値がnilでも型がnilでない限りインターフェース自体はnilではないからです。

解決策: 関数で明示的にnilを返すか、戻り値の型をインターフェースに直接置いてください。

安全なパターン
func doSomething() error {
	var err *MyError = nil
	if err == nil {
		return nil   // インターフェース nil を直接返す
	}
	return err
}

この罠は初めて見ると非常に意外で、実務でもときどき出会います。errors.Isなどの標準ツールを使って比較すれば、より安全な方法もあります(次の記事)。

インターフェースを満たすかコンパイル時に検証 #

ある型がどのインターフェースを満たすべきかという意図をコードで明示したい場合。

コンパイル時検証
var _ Speaker = (*Dog)(nil)

この1行は — 「DogがSpeakerを満たさなければコンパイルエラー」です。var _は変数を使わないという印で、(*Dog)(nil)はnilな*Dog値。もしDogがSpeak()メソッドを欠いたらコンパイルが止まります。

ライブラリが自分の型が外部インターフェースを正確に実装しているか確認する標準パターンです。

インターフェース vs 具体型 — どこで抽象化するか #

ガイド:

  1. 関数の引数 → 通常はインターフェース(可能な限り小さく)
  2. 関数の戻り値 → 通常は具体型

これを「Accept interfaces, return concrete types」と略して呼びます。

推奨パターン
// Good
func ReadConfig(r io.Reader) (*Config, error) {
	// ...
}

// 妙なパターン — インターフェースを返す場合はあまりない
func ReadConfig(r io.Reader) (interface{}, error) {
	// ...
}

引数をインターフェースで受け取れば — 呼び出し側がより多様な型を渡せ、テストもmockで楽になります。戻り値は具体型なので呼び出し側がすべてのメソッドを使えます。

よく使う標準インターフェース #

よく出会うインターフェース
// 文字列表現
type Stringer interface {
	String() string
}

// エラー
type error interface {
	Error() string
}

// 比較可能
type Comparable[T any] interface {
	Compare(T) int
}

// ソート
type Sort interface {
	Len() int
	Less(i, j int) bool
	Swap(i, j int)
}

fmt.Printlnがオブジェクトを出力するとき — そのオブジェクトがStringerを実装していれば自動的にString()の結果を使います。自分の型の出力形式を定義する標準的な方法です。

まとめ #

今回の記事で整理した内容:

  • インターフェースは暗黙的実装 — implementsキーワードなし
  • 使う側でインターフェースを定義可能 — 疎結合
  • io.Readerio.Writerのような小さなインターフェースが強力
  • インターフェースは小さいほど良い — メソッド1〜2個
  • 空インターフェース = any (Go 1.18+)
  • comma-ok型アサーションと型switch
  • インターフェース変数のnilの罠(型はあって値だけnil)
  • 「Accept interfaces, return concrete types」
  • Stringererrorのような標準インターフェース

次の記事(#2 エラー処理パターン)では#4 基礎で見たエラーをもっと深く — wrapping、errors.Is/As、カスタムエラー型パターンまで整理します。

X