インターフェース
読了目安 約4分
「何ができるか」だけを定めるインターフェースと、io.Reader や io.Writer が Go 全体の共通言語になっている理由を学びます。
この章の目次
struct は「何を持っているか」を定める型でした。 この章で扱うのは「何ができるか」だけを定める型で、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 つだけです。
type Stringer interface {
String() string
}fmt.Println や %v は、渡された値が String() string を持っていればそれを呼び、戻り値を表示に使います。
つまり「自分の見た目を自分で決める」宣言です。
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 つです。
type Writer interface {
Write(p []byte) (n int, err error)
}データの出入りがあるもの(ファイル、ネットワーク接続、メモリ上のバッファ、圧縮ストリーム)は、標準ライブラリのほとんどがこの 2 つを実装しています。 だから「Writer を受け取る関数」は、書き込み先が何でも同じコードで動きます。
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 で、書き込み先が画面からメモリに変わっても呼び出す関数は同じです。
http.Handler もインターフェース
Part 3 で中心になる http.Handler も、メソッド 1 つのインターフェースです。
type Handler interface {
ServeHTTP(w http.ResponseWriter, r *http.Request)
}この定義は、「ブラウザからの依頼を受けて返事を書けるものなら何でもサーバーに載せられる」という設計です。 使い方は ルーティングとハンドラ で扱います。
試してみよう
最初の Notifier の例を、2 段階で拡張してください。
- 新しい型
Bot(フィールドは好きに決めてください)を追加し、Notify() stringを実装してsendに渡す。sendを 1 文字も変えずに動けば成功です。 - Notifier の例の
GroupにもString() stringを実装し、fmt.Println(Group{Name: "isucon-team", Members: 3})の表示が変わることを確かめる。implementsと書かずに Group が fmt.Stringer を満たした瞬間です。