メインコンテンツにスキップ

「Goは関数型自体がインターフェースを実装できる」がわからないのでnet/http を見て理解する

前書き

「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)なので、ServeHTTPHandlerFunc自体を実行するということでしょう。

実はこれが関数型自体がインターフェイスを実装できるということなのですが、今の段階ではまだよくわかりません。そこで、適当なWebサーバを作ってこのHandlerFuncServeHTTPを実行してみます。

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として扱えるようにしているということでした。

9 min read