たそらぼ

日頃思ったこととかメモとか。

http.HandlerFuncという型

Go言語では関数にも識別子名を付けることができ、インターフェースと組み合わせて柔軟に振る舞いをコントロールすることができることを確認しました。

Goプログラミング実践入門を読んでいたところ、

HandlerFuncとHandleFuncを混同しないようにしてください。
http.HandleFuncは、特定のパターンを処理するためのハンドラ関数をDefaultServeMuxに登録する関数です。
一方、http.HandlerFuncは型の名前です。

という文がありました。HandlerFuncって明らかに関数っぽいんですが、なんで型の名前なんでしょうか...。

HandlerFunc

HandlerFuncはハンドラー関数とパスを結びつける際に出てきます。
具体的には以下のような例で、

package main

import (
	"fmt"
	"net/http"
)

func test(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "This is test page!")
}

func main (){
	server := http.Server{
		Addr: "127.0.0.1:8080",
	}

	testhandler := http.HandlerFunc(test)
	http.Handle("/test",testhandler)

	server.ListenAndServe()
}

ハンドラー関数testをHandlerFuncに渡し、testhandlerを作ります。testhandlerはパスと一緒にHandleに渡すことで、アプリ内でパスとハンドラ関数が結びつく訳です。

でも、testって関数だし、HandlerFuncでキャストできるってどういう感覚なの?と疑問が深まったので、とりあえずドキュメント(http - The Go Programming Language)を確認します。

f:id:tasotasoso:20190726005910p:plain

                           ※画像はドキュメントから引用
分かったこととしては、
・HandlerFuncは確かに型
・HandlerFuncはServeHTTP(ResponseWriter, *Request)をメソッドとして持っている
・ServeHTTPはHandlerFunc自身を呼び出す

関数が型である上に、ServeHTTPとかいうメソッドまで持っているとか、どういうこと...。
いまいち理解が追いつかないので、サンプルコード内で関係がありそうなHandleを調べに行ってみます。

HandleとHandler

http.Handle()は、サーバーのパスにアクセスした時のアプリの挙動を決めることができます。
http.Handle()の仕様はドキュメント(http - The Go Programming Language)を見ると、

func Handle(pattern string, handler Handler)

Handle registers the handler for the given pattern in the DefaultServeMux. The documentation for ServeMux explains how patterns are matched.

ということで、
・パス
・Handler
を渡す感じで使うようです。

また、今度はHandlerとかいうのが出てきたので、こちらも調べておきます。http.Handler()の仕様もドキュメント(http - The Go Programming Language)を見てみます。

A Handler responds to an HTTP request.
(略)

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

どうやら、HandlerはServeHTTPをServeHTTP(ResponseWriter, *Request)をメソッドに持つインターフェースであることがわかりました。

ここで思い出したいのが、HandlerFuncはServeHTTPをメソッドとして持っていることです。HandlerFuncが型として、ハンドラー関数をキャストした際、ServeHTTPはハンドラー関数自身を呼び出すだけです。そのため、キャスト後の関数がServeHTTPを持っちゃってることにしても、特に不都合はない訳です。結局、キャスト後の関数はServeHTTPを持っているので、Handlerインターフェースを実装していることになります。そのため、Handleに渡してもOKな訳ですね。

ということで、なんか一見落着感が出てきました。でも結局、「関数の型」とか「関数をキャスト」とか「関数のメソッド」ってなんなんでしょうか...。

type

「関数の型」とか「関数をキャスト」とか「関数のメソッド」ってなんなのか。ここが、やっぱり理解が難しいところのようで、解説記事がいくつかありました。
特に@tenntennさんの記事(インタフェースの実装パターン #golang - Qiita)が自分の疑問にズバリの答えを書いてくださっていました。

答え部分をかいつまむと、
・HandlerFuncはfunc(w ResponseWriter, r *Request)の識別子名
・typeで識別子を宣言した際、実際に利用するためには変数のキャストが必要
・インタフェース型でなければ,どんな型にでもメソッドを定義することが可能

ということで、実は自分が分かっていなかったのは、Go言語の型がいかに柔軟か!ということだったんですね。この辺はドキュメントの「Types」や@tenntennさんの記事を確認してもらえればさらによく分かります。

実験

Goの型がめちゃくちゃ柔軟なのは分かったのですが、あまりにも凄いので、個人的にはちょっと受け入れ難いです。「HandlerFuncとかってかなり高度に設計されてる気がするし、実はHandlerとか別のところで良しなにやってくれてんじゃないの?」とか思っちゃいます。なので、もうちょっとシンプルな例を作成して実験して見ました。

package main

import (
    "fmt"
)

type Plane interface{
  takeOff()
}

//Planeインターフェースを実装していればメンテしてくれる。
func Maintenance(plane Plane) {
  fmt.Println("MAINTENANCE!")
}

//PlaneFunc型はtakeOff()をメソッドとして持つ引数なしの関数の型
type PlaneFunc func()
func(f PlaneFunc) takeOff(){
  f()
}

//アヒルさん
func duckFly(){
  fmt.Println("DACK FLY!")
}

func main(){
  duckPlane := PlaneFunc(duckFly)
  duckPlane.takeOff()

  Maintenance(duckPlane)
}

ヒルさんは空を飛べるので、duckFly()という関数があるとします。一方で、Planeインターフェースを用意しておき、Planeインターフェースの実装だとメンテナンスしてもらえるようにMaintenance関数を作ります。

さて、ここで、ある引数なしの関数を、takeOff()メソッドを持つ関数にキャストするPlaneFuncを作ってみます。duckFly()をPlaneFuncでキャストすると、アヒルさんも飛行機と同じようにメンテナンスしてもらえるようになるのでしょうか。実行すると、

DACK FLY!
MAINTENANCE!

確かにメンテナンスしてもらえました。
また、mainを以下のように修正して、

func main(){
  //duckPlane := PlaneFunc(duckFly)
  //duckPlane.takeOff()

  Maintenance(duckFly)
}

実行すると、

cannot use duckFly (type func()) as type Plane in argument to Maintenance:
func() does not implement Plane (missing takeOff method)

と、takeOffメソッドを持っていないという点でPlaneインターフェースを実装していないので実行できないというコンパイルエラーが返ってきました。確かに関数の型でキャストすることで、インターフェースを実装しているか否かの振る舞いをコントロールすることができています。

参考

http.HandlerFunc型の話にぶち当たった本

Go公式のTypesの説明
golang.org