Yappli Tech Blog

株式会社ヤプリの開発メンバーによるブログです。最新の技術情報からチーム・働き方に関するテーマまで、日々の熱い想いを持って発信していきます。

Goで異なる構造体を1つのsliceで扱う

サーバーサイドエンジニアの森谷です。
先日、開発中に面白い実装を見つけたため小ネタとしてご紹介します。

例として駐輪場に駐められている自転車とバイクを管理するsliceを考えます。
以下の構造体を定義したとして、これらを1つのsliceで扱いたいとしたときにどのように実装すれば良いでしょうか?

type Bicycle    struct{} // 簡略化のためフィールドは省略
type Motorcycle struct{} // 簡略化のためフィールドは省略

空interfaceで扱う方法と問題点

パッと思いつくのは空のinterfaceを定義し、そのsliceとして扱うやり方です。

type Vehicle interface{}

var parkedVehicles = []Vehicle{
    &Motorcycle{},
    &Bicycle{},
    &Bicycle{},
    &Motorcycle{},
}

これで実現はできますが、当然ながらparkedVehiclesにはどんな構造体でも入れることができてしまいます。

type Human struct{}
parkedVehicles = append(parkedVehicles, &Human{}) // これは出来ないようにしたい

例えばVehicle interfaceが Run() メソッドを持っているなどの肉付けがされていればある程度の制限はできるでしょうが、メソッドを持つ必要がない場合はどうすれば良いでしょう?

解決案

空の非公開メソッドを定義することでBicycleあるいはMotorcycleのみに制限することができます。

type Vehicle interface {
    isVehicle()
}

func (*Bicycle) isVehicle()    {}
func (*Motorcycle) isVehicle() {}

おまけ: 実装を知ったきっかけ

実はこちらはprotobufから自動生成されたコードを見て知った方法になります。
https://developers.google.com/protocol-buffers/docs/reference/go-generated#oneof

上記ページの例がすごくわかりやすいのでそのまま活用させていただきますが、以下のように oneof を用いた定義を行ったとします。

package account;
message Profile {
  oneof avatar {
    string image_url = 1;
    bytes image_data = 2;
  }
}

こちらのprotoファイルから生成したGoのコードが以下のようになります。

type isProfile_Avatar interface {
    isProfile_Avatar()
}

type Profile_ImageUrl struct {
    ImageUrl string `protobuf:"bytes,1,opt,name=image_url,json=imageUrl,proto3,oneof"`
}
type Profile_ImageData struct {
    ImageData []byte `protobuf:"bytes,2,opt,name=image_data,json=imageData,proto3,oneof"`
}

func (*Profile_ImageUrl) isProfile_Avatar() {}
func (*Profile_ImageData) isProfile_Avatar() {}

まとめ

こんな記事タイトルをつけておいて何ですが、正直に言うと異なる構造体を1つのsliceに入れたいと考えた時点でまずは設計を見直すべきとは思います。
あるいは抽象化してsliceで扱うとしても、それがメソッドを何も持っていない空のinterfaceというケースはそう多くない気もします。

多用するのは注意が必要だとは思いますが、おまけで紹介した例のような場合にポリモーフィズムを実現するテクニックの1つとしては面白い内容だと思いますので、参考になれば幸いです!