【Go言語】埋め込みでinterfaceを簡単に満たす

オリジナルのThe Go gopher(Gopherくん)は、Renée Frenchによってデザインされました。

はじめに

初級者向けです。
Go言語にはオブジェクト思考プログラミングにおける継承は存在しませんが、埋め込み(embedded)を利用して委譲させることはできます。継承がないという言葉が1人歩きしてしまい、無駄なコードを書かないために抑えておきたいポイントをまとめています。

埋め込みとは?

今回はテストケースを書く際の埋め込みの活用方法をサンプルコードで説明します。
まず、下記のUserという構造体を定義します。

package user
type User struct {
FirstName string
LastName string
Gender string
Birthday string
}
func (u *User) Nickname() string {
return u.FirstName[:1] + u.LastName[:1]
}
func (u *User) IsMale() bool {
return u.Gender == "male"
}
func (u *User) Age() int {
return age(Birthday)
}

次に、別パッケージにUser構造体が持つメソッドを満たすinterfaceを実装します。実装したinterfaceを用いた、IsTestTargetという関数を作成します。

package target
type UserInterface interface {
Nickname() string
IsMale() bool
Age() int
}
func IsTestTarget(u UserInterface) bool {
return u.Age() > 25
}

上記のIsTestTargetのテストコードを書いてみたいと思います。
下記のtestUserの定義に注目してください。

package target
import (
"testing"

"github.com/stretchr/testify/assert"
    "./path/to/user"
)
type testUser struct {
user.User // これでinterfaceを満たせる
age int
}
// 差し替えたいメソッドだけ定義する(オーバーライド)
func (u *testUser) Age() int {
return u.age
}
func TestIsTestTarget(t *testing.T) {
a := assert.New(t)

testCases := []struct {
age int
expected bool
}{
{24, false},
{25, false},
{26, true},
{30, true},
}

for _, tt := range testCases {
u := testUser{age: tt.age}
a.Equal(tt.expected, IsTestTarget(&u))
}
}

interfaceを満たせるとは?

サンプルコードのUserInterfaceに定義されている全てのメソッドが実装されている状態が、interfaceを満たすと言えます。
testUserはUser構造体を埋め込んでいるので、UserInterfaceを満たしています(ダックタイピング)。 直接メソッドを実装しているu.Age()だけでなく、u.Nickname() も u.IsMale() も実行できるということです。この場合、u.FirstNameやu.Genderといった、埋め込んでいるUser structのフィールドにもアクセスできます。

error型もinterface

よくでてくるerror型も実はinterfaceです。
Error()という単一のstringを返却するメソッドを持っている構造体は、全てerror interfaceを満たしていると言えます。

func Exec() error {} // ←このerrorはinterface
type error interface {
Error() string
}

interfaceを埋め込むこともできる

下記もできます。

type UserInterface interface {
Nickname() string
IsMale() bool
Age() int
}
type testUser struct {
UserInterface // これでもinterfaceを満たせる
age int
}

だたし、この場合だと、testUserで定義されていないメソッドにアクセスした場合はpanicとなってしまいます。実態が存在しないので当然ですね。interfaceを満たしているがためにビルドは正常にできるので、テストコード中などでは有効的に使えそうです。(panicを発生させてしまう可能性があるので、通常コードで使用する場合のハンドリングは気をつけなければいけません。)
今回のサンプルコードではUserInterfaceの埋め込みで事足りるので、こちらの方が無駄にimportせずに解決できます。

package target
import (
"testing"

"github.com/stretchr/testify/assert"
)
type testUser struct {
UserInterface // これでinterfaceを満たせる
age int
}
// 差し替えたいメソッドだけ定義する(オーバーライド)
func (u *testUser) Age() int {
return u.age
}
func TestIsTestTarget(t *testing.T) {
a := assert.New(t)

testCases := []struct {
age int
expected bool
}{
{24, false},
{25, false},
{26, true},
{30, true},
}

for _, tt := range testCases {
u := testUser{age: tt.age}
a.Equal(tt.expected, IsTestTarget(&u))
// u.IsMale() // panicになる
}
}

また、interfaceにinterfaceを埋め込むこともできます。
複数埋め込みももちろんできます。

まとめ

type User struct{}
type UserInterface interface{}
type Person struct{}
type PersonInterface interface{}
# できること
// structstructを埋め込む
type SuperUser struct{
User
}
// structinterfaceを埋め込む
type User interface{
PersonInterface
}
// interfaceinterfaceを埋め込む
type UserInterface interface{
PersonInterface
}
// 埋め込んだstructのフィールドへアクセス
u := SuperUser{}
u.Gender = "male"
u.User.Gender = "female"
// 埋め込んだstructのメソッドへアクセス
u := SuperUser{}
u.Age()
u.User.Age()
// 多重継承的なやつ&複数埋め込み
type SpecialUser struct{
SuperUser
PersonInterface
}
// フィールド名を指定して埋め込む
type SpecialUser struct{
s SuperUser
p PersonInterface
}
// オーバーライド
func (_ SuperUser) Age() int {
return 25
}
// 未定義なのにダックタイピング
type testUser struct{
UserInterface // Nickname(), IsMale(), Age()関数を持つinterface
age int
}
func (u testUser) Age() int {
return u.age
}
func IsTestTerget(u User) bool {
return u.Age() > 24
}
u := testUser{age: 25} // Nickname(),IsMale()メソッドは持っていない
IsTestTerget(&u)
// 単純に別の型として定義しなおす
type SuperUser User
type Time time.Time
# できないこと
// 埋め込んだinterfaceの未定義メソッドへアクセス
u := testUser{age: 25}
u.IsMale() // panic
// 埋め込んだ型への型アサーション
u := SuperUser{}
check(&u)
func check(u UserInterface) {
_, ok := u.(User)
fmt.Println(ok) // false
}

Go言語はシンプルが故に
できないこととできることが明確なわかりやすい言語です。
書きやすくてどんどんかけてしまうのですが、Goの設計思想等を知っていないと、無駄なコードばかりが散見するというもったいない状態になってしまう、というのは実はよくある話なのかなと思っております。
他の言語を習熟した上では、様々な置き換えをすることによって理解を早めることができると思いますが、
初級者の方は若干interfaceの使い方に戸惑っている場合が多いようです。どんな時に使うの?という話があると思いますが、使い方を覚えることでケースバイケース簡潔なコードが書けるようになるのかなと思います。なので、interfaceはメソッド群を満たすことだと理解して、満たすための手法までを理解するのが第一歩だと考えています。
プログラミング初めてみたいという方こそGo言語を選択してみるのも面白いのかなと思っています。