Golangのインタフェースについて勉強してみた

2019-10-30

10/24 の golang.tokyo でtenntennさんの抽象化のLTを聞いたんですがよくわからない部分があったんで、 資料 を見ながらインターフェースに関する部分を中心にまとめてみることにします。

わからないままにしておくのは気持ち悪いというのがモチベーションです。

完全に同じではありませんが、スライドがあったので興味がある方はこちらも参照してください。 抽象化の話題はP25くらいからです。

目次

実はGolang の業務経験はないのでインタフェースの有用性をちゃんと理解できていないかもしれません。 変なことを言っていたら指摘ください。

インタフェースって何よ? (what is the interface?)

インタフェースとはGolangで抽象化するための唯一の方法です

抽象化によって利用者は具体的な実装の詳細を知らなくて済みます。 つまり、インタフェースの定義さえ見ればメソッドの呼び出し方(引数と返却値の形式)がわかります。

インタフェースに定義されているメソッドを集合とみなし メソッドセット と呼びます。

具体的にはインタフェースはメソッドを強制する入れ物(変数)として機能し、満たさなければビルドがコケます

interface.go
package main

import "fmt"

type Stringer interface {
	String() string
}

type Hex int

// このコメントを外すとエラーが消えるよ
/*
func (h Hex) String() string {
	return fmt.Sprintf("%x", int(h))
}
*/

func main() {
	// Hex does not implement Stringer (missing String method)
	var h Stringer = Hex(100)
	fmt.Println(h)
}

変数を利用せずにチェックだけしたいという場合、ブランクに入れておくこともあるらしいです。 (自分はこういうふうには使ったことないけど業務ではあるということなのかな?)

implementation_check.go
package main

import (
	"fmt"
)

type Func func() string

func (f Func) String() string { return f() }

var _ fmt.Stringer = Func(nil)

func main() {}

型アサーション(Type assertion)

なお、interface型に格納された値は型情報がなく、ビルド時にエラーとなることがあります。

変数.(型) で型アサーションして型を戻してあげる必要があります。 (型アサーションできるのは変数の型が interface の場合だけです)

明確な記述を見つけられなかったのですが、型アサーションは型情報を戻しているだけでキャストではないと思っています。 reflect.TypeOf で見たときの型は interface 型の変数に入れているときから変わらないからです。

キャストする場合は int()string() に値を渡します。 インタフェース型を直接キャストしようとすると型情報が欠落しているのでエラーになります。

備考

脱線ですが、数値から文字列にする (例えば 100 から "100" にする) 場合、 string() ではなく strconv.Itoa や Sprintf で変換する必要があります。

package main

import "fmt"
import "strconv"

func main() {
  s0 := string(100) // これはダメ🙅
  s1 := strconv.Itoa(100)
  s2 := fmt.Sprintf("%d", 100)
  fmt.Println(s0, s1, s2) // d, 100, 100
}

何に使えるのか (Useful for what?)

引数としてのインタフェース (interface as argument)

例えばファイルを操作するような関数を作る際にファイルのような具象型を受け取るのではなく、 インタフェースのような抽象型を受け取るようにしておけば、ユニットテストなどで実際のファイルがなくても動作確認できるようになります。

こういった理由で関数を定義する際に特定の具象型に依存するよりも、インタフェースのような抽象型を仮引数とするのが望ましいです。

以下の例では io.Reader インタフェースを仮引数として、 Read([]byte) メソッドがあれば何でも受け取れます。

abstraction.go
package main

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

func read(r io.Reader) string {
	buf := make([]byte, 3)
	r.Read(buf)
	return string(buf)
}

func main() {
	// test.txt の中身は abc
	f1, _ := os.OpenFile("test.txt", os.O_RDONLY, 0666)
	f2 := bytes.NewBufferString("def")

	fmt.Println(read(f1), read(f2)) // abc def
}

golangにおける抽象化はインタフェースを介して行うことを前提としている以上、 フィールドの操作をメソッド(getter や setter)を介して行えるように整備するべきです。

メソッドを介さず引数のハラワタを直接参照するような実装は抽象化を妨げます。

空のインタフェース (empty interface)

前述したようにインタフェースはメソッドを強制するものであって、フィールドは強制しません。 そのため、インタフェース型はメソッドセットさえ満たしていればどのような型でも代入可能なのです。

中でもメソッドセットが空のインタフェース interface{} は何でも受け付ける(Any)型としてよく使われます。 JSONやYAMLで「どのようなフィールドがあるかわからないけどとりあえずパースしたいとき」などに有用です。

json.go
package main

import (
	"encoding/json"
	"fmt"
	"log"
)

const txt = `{
	"a": 1,
	"b": 2
}`

func main() {
	var data interface{}
	bytes := ([]byte)(txt)
	if err := json.Unmarshal(bytes, &data); err != nil {
		log.Fatal(err)
	}
	fmt.Println(data) // map[a:1 b:2]
}

こんなこともできるよ (Tips)

func型の扱い (function type)

関数は一つの型なので、関数に対してメソッドを定義できます。 つまり、インタフェースも適用できます。

以下は fmt.Stringer インタフェースを使ってString()メソッドの定義を強制しています。

func_interface.go
package main

import "fmt"

type Func func() string

func (f Func) String() string { return f() }
func main() {
	var s fmt.Stringer = Func(func() string {
		return "hi"
	})
	fmt.Println(s) // hi
}

fmt.Println では String メソッドが自動的に呼び出されるので、 String() が定義された Func型 に関数をキャストすることで自動的に定義した関数が呼び出されるというわけですね

なお、 interface{} 型は何でも受け付けますが、再定義すると別の型となります。 複数のfunc型変数があり、引数が別々のインタフェース型を受け取る場合、異なる型と判断されます。

func_arg_interface.go
package main

import "fmt"

type Any1 interface{}
type Any2 interface{}

var a Any1
var b Any2
var f func(Any1)
var g func(Any2)

func main() {
	a = b // できる

	// cannot use g (type func(Any2)) as type func(Any1) in assignment
	f = g // できない
	fmt.Println(a, b, f, g)
}

埋め込み (Embedding)

Goでは型階層は作れないが、structやinterfaceでは埋め込みができます。Composition とも言うそうです。 (interfaceにはinterfaceの埋め込みしかできません)

ここでは struct の埋め込みについても確認しておきましょう。 埋め込んだstructのフィールドを参照する場合 構造体.埋め込んだ構造体.フィールド のようにします。

メソッドも 構造体.埋め込んだ構造体.メソッド のように参照できますが、 埋め込んだ構造体は省略して 構造体.メソッド のようにも参照できます。

ただしこの場合、そのメソッドは埋め込み ではなく埋め込み を向いているので注意が必要です。

embedding_struct.go
package main

import "fmt"

type A struct {
	N int
}

func (a A) getN() int {
	return a.N
}

type B struct {
	A
	N int
}

func main() {
	a := A{100}
	b := B{A: A{200}, N: 300}
	fmt.Println(a.N, b.N, b.A.N)                // 100 300 200
	fmt.Println(a.getN(), b.getN(), b.A.getN()) // 100 200 200

}

インタフェースの場合、それぞれのインタフェースが持つメソッドセットの和集合が新たなインタフェースのメソッドセットとなります。

embedding_interface.go
package main

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

// io.Reader と同じ
type Reader interface {
	Read(p []byte) (n int, err error)
}

// io.Writer と同じ
type Writer interface {
	Write(p []byte) (n int, err error)
}

// io.ReadWriter と同じ
type ReadWriter interface {
	Reader
	Writer
}

func main() {
	buf := make([]byte, 3)
	var rw ReadWriter
	// test.txt の中身は abc
	rw, _ = os.OpenFile("test.txt", os.O_RDONLY, 0666)
	rw.Read(buf)

	var rw2 ReadWriter = new(bytes.Buffer)
	rw2.Write(([]byte)("test"))
	fmt.Println(string(buf), rw2) // abc test
}

埋め込んだ構造体のメソッドセットが重複するとエラーになります。

共通部分を抜き出すという観点で抽象化するとたいていうまく行かないので、 ioパッケージのように利用者の視点で最低限のメソッドセットを定義するべきです。

なお、全てを抽象化する必要はないので最初はコアな部分だけを抽象化するようにしてみましょう。

参考