前書き
「Goは関数型自体がインターフェースを実装できる」
これを初めて聞いた時に自分は全く理解ができませんでした。関数にメソッドは定義できないのに、どうして関数がインターフェイスを実装できるのかもわかりません。そこで、理解するためにnet/httpパッケージのHandlerFuncを読みながら理解したのでその記録を書いてみます。
調べてみる
まずはHandlerFuncのコードを見てみます。
// The HandlerFunc type is an adapter to allow the use of
// ordinary functions as HTTP handlers. If f is a function
// with the appropriate signature, HandlerFunc(f) is a
// [Handler] that calls f.
type HandlerFunc func(ResponseWriter, *Request)
// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
HandlerFuncという関数型がServeHTTPというメソッドを持っています。ServeHTTPの実装としては、f(w,r)なので、ServeHTTPはHandlerFunc自体を実行するということでしょう。
実はこれが関数型自体がインターフェイスを実装できるということなのですが、今の段階ではまだよくわかりません。そこで、適当なWebサーバを作ってこのHandlerFuncのServeHTTPを実行してみます。
https://pkg.go.dev/net/http#example-HandleFunc に記載のようにGoのサーバはhttp.HandleFuncでハンドラを設定して立ち上げることができます。
package main
import "net/http"
func helloHandler(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello, World!"))
}
func main() {
http.HandleFunc("/hello", helloHandler)
http.ListenAndServe(":8080", nil)
}
>curl http://localhost:8080/hello
Hello, World!
このコードを見ても、helloHandlerには先ほど見たServeHTTPメソッドは無いですし、ただの関数なのに、なぜHello, World!と表示されるのかが不明です。
そこで、http.HandleFuncの中身を見てみると、handleFunc(pattern, handler)という記述があります。
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
if use121 {
DefaultServeMux.mux121.handleFunc(pattern, handler)
} else {
DefaultServeMux.register(pattern, HandlerFunc(handler))
}
}
handleFuncの中身もみてみましょう。
func (mux *serveMux121) handleFunc(pattern string, handler func(ResponseWriter, *Request)) {
if handler == nil {
panic("http: nil handler")
}
mux.handle(pattern, HandlerFunc(handler))
}
HandlerFunc(handler)というように型変換を行っています。HandlerFunc型は前述の通り、ServeHTTPメソッドを持っています。mux.handleは実装を見ると、ハンドラーの登録のようです。
もう少し理解を深めるために、実際にServeHTTPメソッドを呼び出す部分を深掘ります。
http.ListenAndServeのソースコードは以下です。
func ListenAndServe(addr string, handler Handler) error {
server := &Server{Addr: addr, Handler: handler}
return server.ListenAndServe()
}
さらにserver.ListenAndServe()の実装は以下のようになっています。
func (s *Server) ListenAndServe() error {
if s.shuttingDown() {
return ErrServerClosed
}
addr := s.Addr
if addr == "" {
addr = ":http"
}
ln, err := net.Listen("tcp", addr)
if err != nil {
return err
}
return s.Serve(ln)
}
s.Serveは最終的にconn.serveを呼び出します。
func (s *Server) Serve(l net.Listener) error {
...(中略)
ctx := context.WithValue(baseCtx, ServerContextKey, s)
for {
...(中略)
rw, err := l.Accept()
c := s.newConn(rw)
c.setState(c.rwc, StateNew, runHooks)
go c.serve(connCtx) // conn.serveを呼び出す
}
}
そしてconnのメソッドであるserveの実装を見ていると、
// Serve a new connection.
func (c *conn) serve(ctx context.Context) {
...(中略)
serverHandler{c.server}.ServeHTTP(w, w.req)
ありました。serverHandlerという構造体のServeHTTPメソッドを呼び出しています。
ではserverHandlerの定義をみてみましょう。
type serverHandler struct {
srv *Server
}
Server構造体の中身もみます。
type Server struct {
...
Handler Handler // handler to invoke, http.DefaultServeMux if nil
...
}
中にHandlerインターフェイスがありますね。前述の通り、HandlerインターフェイスはServeHTTPをメソッドセットとして持ちます。
HandlerFunc.ServeHTTPの実態は、先ほど見たようにf(w,r)です。そのため、HandlerFunc自体が呼ばれる = helloHandlerが呼ばれます。そうすることで、helloHandlerは第一引数にWriteを行うので、*connに対して書き込みを行い、レスポンスとしてHello, World!が表示という流れです。
つまり、helloHandlerと同じシグネチャ(func(w http.ResponseWriter, r *http.Request))を持っていればどんな関数でもhttp.Handlerとして登録が可能になります。
関数そのものにはメソッドは生やせないが、関数型にはメソッドを定義できるからインターフェイスを実装できる。という流れですね。これで、「関数型がインターフェイスを実装できる」がどういうことがを理解できました。
ServeHTTPの呼び出しを自作して確かめる
次はServeHTTPの呼び出しを自作して確認してみます。
まずはServeHTTPをメソッドセットとして持つ、Handlerインターフェイスを書きます。
type Handler interface {
ServeHTTP(http.ResponseWriter, *http.Request)
}
// 関数の型を定義し、その型にServeHTTPメソッドを実装
type HandlerFunc func(http.ResponseWriter, *http.Request)
func (f HandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) {
f(w, r)
}
次にServeMuxを使用してハンドラーの登録処理を書きます。
type ServeMux struct {
handlers map[string]Handler
}
func NewServeMux() *ServeMux {
return &ServeMux{handlers: make(map[string]Handler)}
}
func (mux *ServeMux) Handle(pattern string, handler Handler) {
mux.handlers[pattern] = handler
}
func (m *ServeMux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if handler, ok := m.handlers[r.URL.Path]; ok {
// 登録されたハンドラを呼び出す
handler.ServeHTTP(w, r)
} else {
http.NotFound(w, r)
}
}
全コードは以下。
package main
import "net/http"
type Handler interface {
ServeHTTP(http.ResponseWriter, *http.Request)
}
// 関数の型を定義し、その型にServeHTTPメソッドを実装
type HandlerFunc func(http.ResponseWriter, *http.Request)
func (f HandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) {
f(w, r)
}
type ServeMux struct {
handlers map[string]Handler
}
func NewServeMux() *ServeMux {
return &ServeMux{handlers: make(map[string]Handler)}
}
func (mux *ServeMux) Handle(pattern string, handler Handler) {
mux.handlers[pattern] = handler
}
func (m *ServeMux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if handler, ok := m.handlers[r.URL.Path]; ok {
// 登録されたハンドラを呼び出す
handler.ServeHTTP(w, r)
} else {
http.NotFound(w, r)
}
}
package main
import "net/http"
func helloHandler(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello, World!"))
}
func main() {
mux := NewServeMux()
mux.Handle("/hello", HandlerFunc(helloHandler))
http.ListenAndServe(":8080", mux)
}
このServeMuxを使用してハンドラの登録およびWebサーバの起動をしてみましょう。
package main
import "net/http"
func helloHandler(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello, World!"))
}
func main() {
mux := NewServeMux()
mux.Handle("/hello", HandlerFunc(helloHandler))
http.ListenAndServe(":8080", mux)
}
>curl http://localhost:8080/hello
Hello, World!
正常に動いています。
ここで、helloHandlerのシグネチャを変更してみましょう。
io.Writerに型を変更してみます。
func helloHandler(w io.Writer, r *http.Request) {
w.Write([]byte("Hello, World!"))
}
そうすると、
cannot convert helloHandler (value of type func(w io.Writer, r *http.Request)) to type HandlerFunccompilerInvalidConversion
というエラーが出ます。 訳すと、「Go の型システムでは「func(http.ResponseWriter, *http.Request)」という関数型と 「func(io.Writer, *http.Request)」という関数型は互換性がありません。」という内容です。
つまり型変換を行うことができないということですね。関数型自体がインターフェースを実装できるのはこの仕組みのおかげということでした。
まとめ
net/httpのHandlerインターフェイスを見ながら、「関数型がインターフェイスを実装できる」がどういうことかを理解してきました。Go では任意のユーザ定義型にメソッドを追加できるので、関数型にもメソッドを追加できます。「関数そのもの」はインターフェイスを実装できませんが、「関数型」にはメソッドを生やせるため、インターフェイスを実装できます。http.HandlerFuncはこの仕組みを利用して、関数をそのまま http.Handlerとして扱えるようにしているということでした。