Go

Goのメソッドは構造体以外にでも定義できるしそれが便利なこともよくある

More than 1 year has passed since last update.

いわゆる主流のオブジェクト指向言語しか知らない人からみると、クラスに相当するものはGoでは構造体で、メソッドに相当するのは構造体の型に対して定義されたメソッド、というように見えると思う。Goでコードを書いている時に、クラスという存在しない概念について考えても無意味なのだけど、頭のなかでそういうイメージで理解している場合、構造体以外の型にメソッドが定義できる意味がわからないと思う。

そこで、そういう人には多少目から鱗のような話かもしれないが、構造体以外にメソッドを定義して便利なケースをいくつか紹介したい。

カウンタ

単純なカウンタを設計しているとしよう。カウンタが持つメソッドはaddとdecの2つだけで、それらのメソッドはカウンタ値を変更して、新しい値を返すものとしよう。カウンタを構造体を使って実装するとこういうふうになる。

type counter struct {
    v int
}

func (c *counter) inc() int { c.v++; return c.v }
func (c *counter) dec() int { c.v--; return c.v }

上のコードは難しいものではないけど、よくよく考えてみれば要素が1つだけのstructというのは不要な抽象化だ。structというのは複数の値をまとめるものなのに、これには要素が1つだけしかないから、構造体は別に必要ではない。下のように書いたほうがシンプルになる。

type counter int

func (c *counter) inc() int { *c++; return int(*c) }
func (c *counter) dec() int { *c--; return int(*c) }

Goの型システムに慣れていないとこのコードがきちんと動くのがピンとこないかもしれないけど、実際にこれはきちんと動くし、わかってみればこちらのほうがすっきりしていてよい。

http.HandlerFunc

もう一つ例をあげてみよう。net/httpのHTTPハンドラのインターフェイスは次のように定義されている。

type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}

つまりnet/httpのHTTPハンドラはどんな型でもいいのだけど、その型はServeHTTPという名前のメソッドを持っていなければいけなくて、第一引数がResponseWriter(HTTPレスポンスの出力先)、第二引数が*Request(HTTPリクエスト)ということになっている。この型の値をhttp.Handleに渡すだけであとはライブラリが勝手にこのメソッドを呼んでHTTPリクエストをハンドルしてくれることになっている。

では自分でHTTPハンドラを書きたくなった時にはどういうふうにすればよいのだろうか? そういうときには、まず自分でHTTPハンドラの型を定義して、それにServeHTTPメソッドを定義することになるだろう。Hello worldを表示するHelloHandlerを定義してみよう。

type HelloHandler struct {}

func (HelloHandler) ServeHTTP(w ResponseWriter, r *Request) {
    w.Write([]byte("Hello world"))
}

しかしまたもや上のstructはちょっとおかしな感じがする。要素が1つもないのだからわざわざそんなものを用意する必要はなさそうだ。とはいえ型を定義しないとメソッドは定義できないし、一方であえて構造体の中に入れなければいけないデータというものも見当たらない。どうすればよいのだろうか?

この場合には自分の型を関数として定義して、ServeHTTPはそれを呼ぶようにすると、いろいろなものがうまく収まる。

type HelloHandler func(ResponseWriter, *Request)

func (f HelloHandler) ServeHTTP(w ResponseWriter, r *Request) {
    f(w, r)
}

あとは関数を定義してこのHelloHandler型の変数に入れる(あるいは単にその場で変換する)だけでよい。これでめでたしめでたし。

かというと実はこれでもまだ十分にシンプルにはなっていない。

HelloHandler型はHello Worldを表示するという具体的な関数と一切関係がなくなって、ただ関数をラップしてServeHTTPメソッドを足すだけの型になってしまった。ほかのどんなハンドラ関数もこれでラップすることができる。だから別にHelloといった名前を付ける必要はなくて、もっと一般的な名前にしたほうがよさそうだ。

実は上のHelloHandlerと同じメソッドを持つ同じ型がnet/httpにHandlerFuncという名前ですでに定義されている。これを使えば、自前のハンドラはただの関数として定義して、使うときにHandlerFuncに変換するだけでよくなる。

// 自前のハンドラはただの関数
func hello(w ResponseWriter, r *Request) {
    w.Write([]byte("Hello world"))
}

func main() {
    // 型を変換する。直接http.HandlerFunc(hello)と書いてもよい
    var h http.HandlerFunc = hello
    http.Handle("/hello", h)
}

結局のところHTTPハンドラはただの関数にすぎない、というところまで単純化して書くことが可能になったわけだ。Goのシンプルな言語機能の組み合わせの妙というものを感じてもらえただろうか?