Go言語のInterfaceの条件分岐の書き方を考えてみる

HRBrain Advent Calendar 9日目、サーバーサイドエンジニアの星井です。
元プロレスラーのベネズエラ人とスパーリングをしていて、投稿が遅くなりました。すみません。

Interfaceの条件分岐の書き方で迷うことが多いので、今日はどの書き方が良いかを考察してみようと思います。

どんな書き方ができるか

Interfaceの種類別に異なる処理をしたい時、下記のようにtypeごとにSwitchで条件分岐するのが一般的ではないでしょうか。

①一般的なSwitch分岐

switch animal := a.(type) {
        case Dog:
            animal.Bark()
        case Cat:
            animal.Meow()
        case Bird:
            animal.Fly()
        case Bear:
            animal.Roar()
        case Rabbit:
            animal.Jump()
        }

メリットは分岐しながら元の型に戻せるところで、デメリットは分岐が多くなると可読性が下がってしまうところです。

②Typeを使ったSwitch分岐

それぞれの構造体にTypeという判別用のフィールドを持たせ、Type()という関数を呼んで条件分岐する書き方もできます。

switch a.Type() {
        case AnimalTypeDog:
            a.(Dog).Bark()
        case AnimalTypeCat:
            a.(Cat).Meow()
        case AnimalTypeBird:
            a.(Bird).Fly()
        case AnimalTypeBear:
            a.(Bear).Roar()
        case AnimalTypeRabbit:
            a.(Rabbit).Jump()
        }

こちらはあまりメリットが思いつかない。 デメリットは下記です。

  • 構造体を生成する時に、必ず正しいTypeをセットしなければいけない
  • Interfaceの型から、元の型に戻して関数を呼ばないといけないので①よりも可読性が下がる

③Mapを使った分岐

最後にmapを使った書き方ができます。

handler := actionHandler[a.Type()]
handler(a)


var actionHandler = map[AnimalType]func(a Animal){
    AnimalTypeDog:    func(a Animal) { a.(Dog).Bark() },
    AnimalTypeCat:    func(a Animal) { a.(Cat).Meow() },
    AnimalTypeBird:   func(a Animal) { a.(Bird).Fly() },
    AnimalTypeBear:   func(a Animal) { a.(Bear).Roar() },
    AnimalTypeRabbit: func(a Animal) { a.(Rabbit).Jump() },
}

分岐と関数を分けて書けるため、分岐が増えても関数自体の長さを減らすことができ、読みやすいのがメリットです。 ただ、②と同じで、構造体を生成する時に、必ず正しいTypeをセットしなければいけないのがデメリットですね。

どの書き方が一番速いのか

これらの書き方で一番速そうなのは「①スタンダードなSwitch分岐」かなと予想しました。一方、ある記事によると、単純な条件分岐だとSwitchよりMapが速いそうで、実はMapが速いとなったら面白いなと思って、自分で実験してみることにしました。

一番処理が速かったやつ

そんなん結果見なくても分かるがなww

というツッコミは予想していますが、一番速かったのは「①一般的なSwitch分岐」 でした。 50000個のランダムな値で実行した時のベンチマークがこちらです。

Benchmark_50000_actionSwitch_50000_random-4 621763 ns/op
Benchmark_50000_actionSwitchType_50000_random-4 622698 ns/op
Benchmark_50000_actionMap_50000_random-4  998241 ns/op

順番的には①>②>③だったので、きっとType()を呼ぶのに時間がかかるのでしょう。単純な条件分岐だとSwitchよりMapが速い説もここでは使えなかったのは、Mapにした場合、別で関数を呼んで実行しなければいけなかったからですかね。

ただ一部ランダムな値で実行時に②>①になったりしているので、条件分岐の数を増やしたりするともしかしたら全然違う結果になるかもしれません。

まとめ

メリットとデメリットを表にまとめました。基本的には一般的なSwitch分岐が良いと思います。ただ、処理速度に優位があると言ってもナノ秒の世界ですし、可読性の観点からMapの分岐を選択するのも良さそうです。

メリット デメリット
一般的なSwitch分岐 ・処理速度が速い
・分岐しながら元の型に戻せる
・分岐が多くなると可読性が落ちる
Typeを使ったSwitch分岐 ・なさそう(もしかしたら処理速度が速いパターンがあるかも?) ・分岐が多くなると可読性が落ちる(①よりもさらに読みづらい)
・構造体を生成する時に必ず正しいTypeを指定しなければいけない
Mapを使った分岐 ・分岐と関数を分けて書けるので読みやすい ・構造体を生成する時に必ず正しいTypeを指定しなければいけない
・Switchに比べて処理に時間がかかる

本当は正解をパッと決めたかったのですが、残念ながら処理速度と可読性のバランスを考えて実装しましょうという無難でつまらない結論になりました。

もっとこういう書き方できるよ!
この場合はこっちの方が良いよ!
こういうケースのベンチマークも見てよ!

もしこのような思いを抱いた方がいたら、ぜひ聞きたいので教えてください🙌