「インタフェース」
書籍:プログラミング言語Go
第7章 「インタフェース」 の要点と思われる箇所を自分のメモ用にまとめました。
7 インタフェース
- 具象型をあるインタフェース型として扱いたい場合、具象型が満足する全てのインタフェース宣言(継承宣言的な?) をする必要はなく、インタフェース型を満たすメソッドが具象型に実装されていれば良い。以下の Any は io.Writer インタフェース型として扱うことができる。
.go
type Any struct { n int }
func (a Any) Write(p []byte) (int, error) {
a.n -= len(p)
if a.n > 0 {
return len(p), nil
} else {
return len(p), io.EOF
}
}
7.1 契約としてのインタフェース
- インタフェースは、内部構造を公開していない振る舞いを一般化あるいは抽象化したもの。
- インタフェース型の値がある場合、その値が何かは示されず、その値で何ができるか、その値でメソッドがどう振る舞うかが示される。
- io.Writer インタフェースは Fprintf とその呼び出し元の間の契約を定義する。
.go
package fmt
func Fprintf(w io.Writer, format string, args ...interface{}) (int, error)
package io
type Writer interface {
// Write は p から len(p) バイトの基底のデータストリームへ書き込みます。
// p から書き込まれたバイト数 (0 <= n <= len(p)) と、書き込みを早く終わらせた原因となったエラーを返します。
// Write は、n < len(p) であるような n を返す場合には nil ではない error を返さなければなりません。
// Write は、たとえ一時的であってもスライスのデータを変更してはいけません。
// 実装は、p を持ち続けてはいけません。
Write(p []byte) (n int, err error)
}
- *os.File や *bytes.Buffer のような具象型は、適切な Write メソッドの振る舞いを呼び出し元に提供することが求められる。
- fmt.Fprintf は io.Writer の内部表現は意識しない。io.Writer の契約で保証される振る舞いのみに依存するため、io.Writerの実体は代替可能。
7.2 インタフェース型
- インタフェース型は具象型がそのインタフェースのインスタンスとして見なされるために持たなければならないメソッドの集まりの定義。
- 既存の型の組み合わせとして新たなインタフェース型を宣言できる。構造体埋め込みに似た構文でインタフェースを埋め込みすることができる。
.go
package io
type ReadWriteCloser interface { // 埋め込みによるインタフェース組み合わせ方法。
Reader
Writer
Closer
}
type ReaderWriter interface { // 埋め込みではないインタフェース組み合わせ方法。
Read(p []byte) (n int, err error)
Write(p []byte) (n int, err error)
}
7.3 インタフェースを満足する
- インタフェースに対する代入可能性の規則は単純。その型がインタフェースを満足して (インタフェースのメソッドが具象型に実装されて) いれば代入可能。
- インタフェースは具象型とその型が保持する値を包み隠す。具象型が他のメソッドを持っていてもそのインタフェース型が公開しているメソッドしか呼べない。
.go
os.Stdout.Write([]byte("hello")) // OK: *os.File は Write メソッドあり
os.Stdout.Close() // OK: *os.File は Close メソッドあり
var w io.Writer
w = os.Stdout
w.Write([]byte("hello")) // OK: io.Writer は Write メソッドあり
w.Close() // コンパイルエラー: io.Writer は Close メソッドなし
- 空インタフェース型 (interface{}) は全くメソッドを持たない。それを満足する型に何も要求していないので、全ての値を空インタフェースに代入できる。
.go
var any interface{}
any = true
any = 12.34
any = "hello"
...
- 宣言の方法により型がインタフェースを満足することを強制することも可能。
.go
var w io.Writer = new(bytes.Buffer) // *bytes.Buffer は io.Writer を満足しなければならない
var _ io.Writer = (*bytes.Buffer)(nil) // *bytes.Buffer は io.Writer を満足しなければならない
- 一つのクラスが満足するインタフェースの集まりをクラスで明示的に記述する言語と異なり、Go は具象型の宣言を修正することなく、必要な場合に新たな抽象化つまり、興味ある部分の新たなグループ化を行うことができる。(既に定義済みの具象型のメソッドをインタフェースとして切り出せるし、新たなインタフェースを満足したい場合は具象型にそのインタフェースのメソッドを追加実装すればOK。)
7.4 flag.Value によるフラグの解析
- flag.Duration 関数は time.Duration 型のフラグ変数を生成し、String メソッドで表示されたものと同じ表記を含む、さまざまな使いやすい形式でユーザが期間を指定できる。
7.5 インタフェース値
- Go では変数は常に定義された値へ初期化される。インタフェースも例外ではなく、インタフェースのゼロ値は nil。
.go
var w io.Writer // w は nil。
- 具象型からインタフェースの暗黙的な変換を伴う代入も可能。
.go
w = os.Stdout // *os.File を io.Writer に変換し、代入している。
- インタフェース値は任意の大きさの動的な値を保持することができる。
.go
var x interface{} = time.Now()
- インタフェース値は ==, != を使って比較可能(map のキーや switch のオペランドとして使える)。
- どちらも nil か、動的な型が同一でかつ動的な値がその型に対する == の通常の振る舞いに従って等しければ二つのインタフェース値は等しい。
- ただし、スライスなど、インタフェース値が同じ動的な型を持つが、その型が比較できない (スライスなど) 場合、比較は失敗しパニックになる。
- インタフェース値は比較可能であるが比較が失敗し、パニックが発生しうる点に注意。インタフェース値が比較可能な型の動的な値を含んでいると確証できるときのみ比較を利用すること。
7.5.1 警告:nil ポインタを含むインタフェースは nil ではない。
- 値を含まない nil インタフェース値は、たまたま nil であるポインタを含むインタフェース値と異なる。この違いは Goプログラマをつまずかせる罠を生み出す。
- 以下のコードで、f に渡す buf は nil だが、io.Writer としては byte.Buffer 型の、値として nil を持つインタフェースとなり、 out 自体が nil ではない。そのため out.Write() が呼び出されてパニックになる。
.go
var debug = false
func main() {
var buf *bytes.Buffer
if debug {
buf = new(bytes.Buffer)
}
f(buf)
...
func f(out io.Writer) {
if out != nil {
out.Write([]byte("done!\n")
}
- 上記の解決方法は、呼び出し元の buf を io.Writer に変更すること。
.go
var buf io.Writer
if debug {
buf = new(bytes.Buffer)
}
f(buf) // OK
7.6 sort.Interface でのソート
- sort は多くのプログラムで頻繁に使われる操作で、sort パッケージはどのような列に対しても任意の順序付け関数に従った列内でのソートを提供する。
- Go の sort.Sort 関数は、列やその要素の表現についてはなにも想定しない。代りに汎用ソートアルゴリズムとソートできる個々の列型との間の契約を定めるインタフェースを使う。
.go
package sort
type Interface interface {
Len() int
Less(i, j int) bool // i, j は列要素のインデックス
Swap(i, j int)
}
- 利便性のため sort パッケージは []int, []string, []float64 に特化していて、それらの自然な順序付けを行う関数と方も提供している。
7.7 http.Handler インタフェース
- ServeHTTP でパスごとに case を追加しなくてもよいよう、ServeMux (リクエストマルチプレクサ) が提供されている。
- Go は Ruby の RailsやPython の Django のような標準フレームワークを持たない(存在しないわけではないがフレームワークが不要なほど柔軟なつくりである)。フレームワークは拡張により長期的保守が困難になりうる。
- HandlerFunc 型の定義により、ServeHTTPと同じ引数、戻り値の関数を ServeHTTP として利用することが可能となる。HandlerFunc は関数値にインタフェースを満足させるアダプタ。HandlerFunc により異なる名前のメソッドを容易に ServeHTTP メソッドとして利用できる。
.go
type HandlerFunc func(w ResponseWriter, r *Request)
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
- ウェブサーバはそれぞれのハンドラを新たなごルーチンで起動する。同じハンドラに対する別リクエストも含めて他のゴルーチンがアクセスするかもしれない変数へハンドラがアクセスする際はロックなどが必要。
7.8 error インタフェース
- error 型とはエラーメッセージを返す単一メソッドを持ったインタフェース型。
.go
type error interface {
Error() string
}
- 新たな error は errors.New() で最も簡単に返せる。error の実体の文字列は string の type ではなく構造体。不用意に error の実体の文字列が書き換えられることを防ぐため。
.go
type errorString { text string }
func (e *errorString) Error() string { return e.text }
func New(text string) error { return &errorString{text} }
- New で返される errorString はポインタ型のため、New 呼び出し毎に異なる error インスタンスが割り当てられる。
.go
fmt.Println(errors.New("EOF") == errors.New("EOF")) // "false"
- syscall パッケージは error を満足する数値型 Errno を定義しており、Unix プラットフォーム上での errno に対応する文字列を返す error が定義されている。
.go
var err error := syscall.Errno(2) // 2 = ENOENT
fmt.Println(err.Error()) // "no such file or directory"
7.9 例:式評価器
- 式評価器を例として、多態的な要素のツリー構造などを表現する場合に適切なインタフェースを定義することで再起処理を実現できる。
7.10 型アサーション
- 型アサーションの書き方は x.(T)。x が動的な型 T と一致するかどうかを検査し x を T として取りだす。(bool の第二戻り値を受け取らない場合)検査に失敗するとパニックになる。
.go
var w io.Writer
w = os.Stdout
rw := w.(io.ReadWriter) // 成功: *os.Stdout は Read と Write の両方を持っている
w = new (ByteCounter) // p.199 で定義
rw = w.(io.ReadWriter) // パニック: *ByteCounter は Read メソッドを持たない
- 型アサーションで bool の第二戻り値を受け取るとパニックにはならない。
.go
var w io.Writer = os.Stdout
f, ok := w.(*os.File) // 成功: ok, f == os.Stdout
b, ok := w.(*bytes.Buffer) // 失敗: !ok, b == nil
- ok の場合、次に何をするか決まっていることが多いので if の拡張形式で簡潔に書ける場合が多い。
if f, ok := w.(*os.File); ok {
....
- 以下のようにオペランドの変数を再利用する書き方もできる。
.go
if w, ok := w.(*os.File); ok {
...
7.11 型アサーションによるエラーの区別
- os.IsNotExist(error) などはエラーに対して型アサーションを利用し、エラーの具象型で判定する方法を用いている。
- PathError が返されて fmt.Errorf によりエラーメッセージを詳細化した場合、PathError 構造体ではなくなるため os.IsNotExist(error) が機能しなくなる。操作失敗直後にエラーの区別する必要があるので注意。
7.12 インタフェース型アサーションによる振る舞いの問い合わせ
- 特定のメソッドを持っているか検証するために型アサーションを利用することができる。
.go
type stringWriter interface {
WriteString(string) (n int, err error)
}
if sw, err := w.(stringWriter); ok {
... // w は WriteString(string) メソッドを持っている。
- fmt.Printf 内では、単一オペランドを文字列へ変換するために型アサーションを利用している。
.go
func formatOneValue(x interface{}) string {
if err, ok := x.(error); ok {
return err.Error()
}
if str, ok := x.(Stringer); ok {
return str.String()
}
...
7.13 型 switch
- 型アサーションは、どのようなメソッドを持つか判別するためと、値がどのような型であるかを判別するために利用される。
- interface{} はさまざまな具象型を値として保持でき、インタフェース値の型を動的に識別し、型ごとに処理を切り替えるために型アサーションは利用できる。
- 型アサーションによる型の識別は switch を利用して以下のように表現できる。
.go
switch x.(type) {
case nil:
case int, uint:
case bool:
case string:
default:
}
- case は上から順に比較されるため、x が複数インタフェースを持つ場合、case の順番が重要になるので注意。(fallthrough は許されていない。)
- 以下の形式でswitch を記述すると、case 内では x は case の型として扱われる。ただし、複数の型の case では 元の型のままなので注意。
.go
func sqlQuote(x interface{}) {
switch x := x.(type) {
case int, uint: // 複数の型の case なので、この case 内では x は interface{} のまま。
case string: // 単一の型の case なので、この case 内では x は string として扱える。
7.14 例:トークンに基づく XML デコード
- encoding/xml パッケージは、トークンに基づくAPIを提供している。トークンを型アサーションで判別することでそれぞれのトークンの種類に応じた処理を記述できる。
7.15 ちょっとした助言
- 不必要な抽象化でインタフェースを増やしてはいけない。実行時コストがかかる。
- インタフェースは統一的に扱わなければならない二つ以上の具象型が存在する場合にだけ必要。
- 具象型が一つの場合でも、依存関係の問題で同一パッケージに具象型を定義できない場合はインタフェースを定義してもOK。
- Goのインタフェースは複数の具象型が満足するためのもののため、インタフェース内のメソッドは単純で数が少ない定義であるべき。Goのインタフェース設計では "必要なものだけを求める" ということが重要。
練習問題解答例