Goの型システムとGoらしい書き方

Goにはクラスがない。しかし、データ構造をまとめるためのstructがあり、structにはメソッドを紐付けることができる。 そのため、プログラマによってはオブジェクト指向言語のようにGoを書くこともある。 しかし、Goの型に継承関係という概念は無く、RubyやPythonのような考え方でコードを書く訳にはいかないだろう。

Goの作者Rob Pikeは以下のように述べている。

… the more important idea is the separation of concept: data and behavior are two distinct concepts in Go, not conflated into a single notion of “class”.

Goにはclassは存在しない。classとはデータとふるまいをまとめあげるものである。 Goでは、データと振る舞いは分離している。 structはデータを提供するための軽量な手段であり、それ以上のことはしない。 structは型の階層を表現することは決して無い。

interfaceの埋め込み

interfaceの埋め込みは継承ではない。 Goでは、コードの再利用は継承ではなくコンポジションで実現される。 継承はしばしば、プログラムの構造を複雑にさせ、メンテナンス性の低下をもたらす。 Goは継承の代わりに、コンポジションと、 interfaceを使ったメソッドのdispatchを提供している。

以下のようなコードを見てみよう。

package main

import (
  "io"
  "sync"
)

type File struct {
  sync.Mutex
  rw io.ReadWriter
}

func main() {
  f := File{}
  defer f.Unlock()
  f.Lock()
}

File構造体はsync.Mutex構造体を埋め込まれている。Mutex構造体は Lock() Unlock() メソッドを実装している。 これにより、File型の変数fも、 Lock() Unlock() というメソッドを呼び出すことができる。 これは、サブクラスではなく、コンポジションである。

ポリモーフィズム

サブクラスのないGoでは、ポリモーフィズムをinterfaceによってのみ実現する。以下のようなコードを見てみよう。

package main

import (
  "bytes"
  "io"
  "log"
)

func main() {
  var r io.Reader

  r = bytes.NewBufferString("hello")

  buf := make([]byte, 2048)
  if _, err := r.Read(buf); err != nil {
    log.Fatal(err)
  }
}

変数rは io.Reader として宣言されているが、 bytes.NewBufferString()*bytes.Buffer を返す。 bytes.Bufferio.Readerを実装しているため、このような変数の代入が可能になる。 つまり、 r.Read(*Buffer).Read にディスパッチされる。

interfaceの実装を宣言しない

Goには implements というキーワードは存在しない。 interfaceを実装しているかどうかは、interfaceに宣言されたメソッドリストをそのstructが実装しているかどうかのみで判断される。

Goには、新たなinterfaceを作るのではなく、コミュニティーや標準ライブラリから提供されているものを使うことを推奨する文化が有る。 これにより、似たようなinterfaceが増えることを抑制している。

意図せずinterfaceを実装してしまい困るようなことはあるだろうか? 100%ないとは言えないが、そのようなケースに出くわしたことはない。 また、もし2つの似たinterfaceが存在している場合、片方は不要であることが多い。

interfaceは小さく宣言し、必要に応じて組み合わせて使うのが望ましい。

コンストラクタは作らない

Goでstcurtを初期化する際、値はすべてゼロ値で初期化される。 Goは、なるべくゼロ値で初期化しても動くようなstructを作る、という考え方がある。言うまでもなく、 NewXxx() という関数を知らなくても動かせるようにだ。

例えば、 http.Clientは以下のようなコードで動かすことができる。

client := http.Client{}

ここから必要に応じて、

client.Timeout = 5 * time.Second

のように設定するのが良い。

それでも NewXxx が必要になるケースは、バリデーションや、コネクションの確立など、なにか手続きが必要なケースだ。 例えば、http.NewRequest()は以下のように使用する。

req, err := http.NewRequest("GET", "https://example.com", nil)

実装を見てみよう。 メソッドとURLをバリデーションし、bodyをReadしているのがわかると思う。

まとめ

Goの型システムは覚えることが少ない一方、柔軟で、あたかもオブジェクト指向のように使用することも可能では有る。 しかし、Goの思想やエコシステムを理解し、Goらしいコードを書くのは重要なことである。 これらについて学ぶためには、なんといっても標準パッケージのコードを読むのが良い。今後も goらしいコードを書けるよう精進していく。