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