🔩 ねじき教室 Go と Web の教室

インターフェース

読了目安 約4分

「何ができるか」だけを定めるインターフェースと、io.Reader や io.Writer が Go 全体の共通言語になっている理由を学びます。

30秒でつかむ 型の名前ではなく、できることでつなぐ
型の名前ではなく、できることでつなぐ異なる具体型でも、同じメソッドを持てば一つのインターフェースとして扱えます。1ファイルReadできる2io.Reader読めるもの3通信の本文Readできる
  1. ファイル Readできる
  2. io.Reader 読めるもの
  3. 通信の本文 Readできる
この章の目次

struct は「何を持っているか」を定める型でした。 この章で扱うのは「何ができるか」だけを定める型で、Go の標準ライブラリはこれを土台に組まれています。

「何ができるか」だけを定めた型

インターフェースは、メソッドの一覧だけを書いた型です。

Go
package main

import "fmt"

type Notifier interface {
	Notify() string
}

type User struct {
	Name string
}

func (u User) Notify() string {
	return u.Name + " さんに通知を送りました"
}

type Group struct {
	Name    string
	Members int
}

func (g Group) Notify() string {
	return fmt.Sprintf("グループ %s の %d 人に通知を送りました", g.Name, g.Members)
}

func send(n Notifier) {
	fmt.Println(n.Notify())
}

func main() {
	send(User{Name: "neziki"})
	send(Group{Name: "isucon-team", Members: 3})
}

send の引数は Notifier なので、「Notify() string を持つ」型なら User でも Group でも渡せます。

fmt.Sprintf は、Printf と同じ書式指定で整形して、画面に出す代わりに結果を文字列で返す関数です。

本番のコードでは、send を書いた後になって通知したい型が増えていきます。 インターフェースで受けていれば、send に一切手を入れずに追加できます。

暗黙の実装

このコードに、「User は Notifier を実装する」という宣言はありません。 Go には Java や C# の implements にあたるキーワードがなく、必要なメソッドをすべて持っていれば、その時点で自動的にインターフェースを満たします

だから後付けが利き、変更できない外部パッケージの型でも、自分の定義したインターフェースで受け取れます。

String() で表示のされ方を決める

いちばん身近な実例が fmt.Stringer です。 定義はメソッド 1 つだけです。

Go
type Stringer interface {
	String() string
}

fmt.Println%v は、渡された値が String() string を持っていればそれを呼び、戻り値を表示に使います。 fmt.Stringer は、「自分の見た目を自分で決める」ことができる構造体を参照するインターフェースです。

Go
package main

import "fmt"

type User struct {
	Name  string
	Score int
}

func (u User) String() string {
	return fmt.Sprintf("User(%s, %d点)", u.Name, u.Score)
}

func main() {
	u := User{Name: "neziki", Score: 1200}
	fmt.Println(u)              // String() の戻り値が出る
	fmt.Printf("%v / %s\n", u, u)
}

String() を消してから実行すると、{neziki 1200} という既定の表示に戻ります。

String() は文字列を返すメソッドなので、組み立てには Println ではなく Sprintf を使います。

String() の中で fmt.Println(u) のように自分自身を %v で渡すと、String() が自分を呼び続けて無限再帰します。フィールドを直接使ってください。

io.Reader と io.Writer

Go 全体で共通言語になっているインターフェースが 2 つあります。 io.Reader は「バイト列を読み出せるもの」、io.Writer は「バイト列を書き込めるもの」を表し、定義はどちらもメソッド 1 つです。

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

データの出入りがあるもの(ファイル、ネットワーク接続、メモリ上のバッファ、圧縮ストリーム)は、標準ライブラリのほとんどがこの 2 つを実装しています。 だから「Writer を受け取る関数」は、書き込み先が何でも同じコードで動きます。

Go
package main

import (
	"bytes"
	"fmt"
	"os"
)

func main() {
	// 1 回目: 標準出力(画面)に書く
	fmt.Fprintln(os.Stdout, "画面へ")

	// 2 回目: メモリ上のバッファに書く
	var buf bytes.Buffer
	fmt.Fprintln(&buf, "バッファへ")
	fmt.Println("バッファの中身:", buf.String())
}

fmt.Fprintln の第 1 引数が io.Writer で、書き込み先が画面からメモリに変わっても呼び出す関数は同じです。

試してみよう

最初の Notifier の例を、2 段階で拡張してください。

  1. 新しい型 Bot(フィールドは好きに決めてください)を追加し、Notify() string を実装して send に渡す。send を 1 文字も変えずに動けば成功です。
  2. Notifier の例の Group にも String() string を実装し、fmt.Println(Group{Name: "isucon-team", Members: 3}) の表示が変わることを確かめる。implements と書かずに Group が fmt.Stringer を満たした瞬間です。
答えを見る

追加するのは Bot 型と Group の String() だけで、sendNotifier には触りません。

Go
type Bot struct {
	Channel string
}

func (b Bot) Notify() string {
	return "Bot が " + b.Channel + " に通知を送りました"
}

func (g Group) String() string {
	return fmt.Sprintf("Group(%s, %d人)", g.Name, g.Members)
}

func main() {
	send(User{Name: "neziki"})
	send(Group{Name: "isucon-team", Members: 3})
	send(Bot{Channel: "#general"})
	fmt.Println(Group{Name: "isucon-team", Members: 3})
}
テキスト
neziki さんに通知を送りました
グループ isucon-team の 3 人に通知を送りました
Bot が #general に通知を送りました
Group(isucon-team, 3人)

Notify() string を持った時点で Bot は Notifier を満たし、String() string を持った時点で Group は fmt.Stringer を満たしています。 最後の行が {isucon-team 3} のままなら、メソッド名か戻り値の型が String() string とずれています。