🖋️

Golang初心者が`net/http`パッケージでWebサーバーをホストする流れを追う

2021/10/26に公開

概要

こんにちは.skonbと申します.
Webエンジニアをさせてもらってます.
普段はJavaでWebアプリを作っていますが,goについてはペーペーの初心者です.

いちおう,tutolialに載ってることはだいたい勉強したのですが,Webサーバーアプリはどう作るか全然わかりません....

ということで「Go言語によるWebアプリケーション開発」(Mat Ryer 著、鵜飼 文敏 監訳、牧野 聡 訳)を参考書として写経しながら,goでWebサーバーを作るの勉強をしています.

シンプルなWebサーバー

package main
import (
	"log"
	"net/http"
)
func main() {
	// root
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte(`
      <html>
        <head>
          <title>Chat</title>
        </head>
        <body>
          Let's chat!
        </body>
      </html>
    `))
	})
	// start the web server
	if err := http.ListenAndServe(":8080", nil); err != nil {
		log.Fatal("ListenAndServe:", err)
	}
}

from main.go
これだけで,簡単なWebサーバーをホストすることができます.
今回は初心者なりに,net/httpパッケージのうち,http.ListenAndServeからhttp.HandleFuncに登録したメソッドが呼ばれるまでの流れを勉強してみました.

http.ListenAndServe(":8080", nil)

まずサーバーが「起動」するフックになっているであろうhttp.ListenAndServe(":8080", nil)から調査しました.
参考資料

// ListenAndServe listens on the TCP network address addr and then calls
// Serve with handler to handle requests on incoming connections.
// Accepted connections are configured to enable TCP keep-alives.
//
// The handler is typically nil, in which case the DefaultServeMux is used.
//
// ListenAndServe always returns a non-nil error.
func ListenAndServe(addr string, handler Handler) error {
	server := &Server{Addr: addr, Handler: handler}
	return server.ListenAndServe() //下の無引数のほうが呼ばれる
}
// ListenAndServe listens on the TCP network address srv.Addr and then
// calls Serve to handle requests on incoming connections.
// Accepted connections are configured to enable TCP keep-alives.
//
// If srv.Addr is blank, ":http" is used.
//
// ListenAndServe always returns a non-nil error. After Shutdown or Close,
// the returned error is ErrServerClosed.
func (srv *Server) ListenAndServe() error {
	if srv.shuttingDown() {
		return ErrServerClosed
	}
	addr := srv.Addr
	if addr == "" {
		addr = ":http"
	}
	ln, err := net.Listen("tcp", addr) 
	if err != nil {
		return err
	}
	return srv.Serve(ln) //serveメソッドが呼ばれる
}

ここで,Handlernilの場合,DefaultServeMuxHandlerとして割り当てられるようです.
DefaultServeMuxについては,先程HandlerFuncの関数が登録される先として登場しました.DefaultServeMuxServeMux型のハンドラで,最初からあるデフォルトのハンドラです.

// ErrServerClosed is returned by the Server's Serve, ServeTLS, ListenAndServe,
// and ListenAndServeTLS methods after a call to Shutdown or Close.
var ErrServerClosed = errors.New("http: Server closed")

// Serve accepts incoming connections on the Listener l, creating a
// new service goroutine for each. The service goroutines read requests and
// then call srv.Handler to reply to them.
//
// HTTP/2 support is only enabled if the Listener returns *tls.Conn
// connections and they were configured with "h2" in the TLS
// Config.NextProtos.
//
// Serve always returns a non-nil error and closes l.
// After Shutdown or Close, the returned error is ErrServerClosed.
func (srv *Server) Serve(l net.Listener) error {
		//...
		//中略
		//...
		c := srv.newConn(rw)
		c.setState(c.rwc, StateNew, runHooks) // before Serve can return
		go c.serve(connCtx)
		//...
		//中略
		//...
}

最後にconn構造体のserveが呼ばれています.
パッと見ですが,HTTP規格準拠でゴリゴリにリクエストを精査し返信を作成していますね,
その中で,ServeHTTPメソッドが呼ばれます.handlernilの場合,ここでDefaultServeMuxが用いられます.

// Serve a new connection.
func (c *conn) serve(ctx context.Context) {
//...
    serverHandler{c.server}.ServeHTTP(w, w.req)
//...
}
func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
    handler := sh.srv.Handler
    if handler == nil {
        handler = DefaultServeMux
    }
    if req.RequestURI == "*" && req.Method == "OPTIONS" {
        handler = globalOptionsHandler{}
    }
    handler.ServeHTTP(rw, req)
}

このServeHTTPメソッドで,ユーザーが渡すHandlerHandlerFuncのメソッドがようやく呼ばれることになります.長かった......

Handler関連のメソッド・クラス

Hander関係のメソッド・クラスは4種類あります.

Handle Handler
無印 Handle Handler
+Func HandleFunc HandlerFunc

HandlerFunc ... responseにメソッドを使う

HandlerFuncはエンドポイントと関数を一対一で紐付け,DefaultServeMuxに関数として設定します.

package main

import (
	"io"
	"log"
	"net/http"
)

func main() {
	h1 := func(w http.ResponseWriter, _ *http.Request) {
		io.WriteString(w, "Hello from a HandleFunc #1!\n")
	}
	h2 := func(w http.ResponseWriter, _ *http.Request) {
		io.WriteString(w, "Hello from a HandleFunc #2!\n")
	}

	http.HandleFunc("/", h1)
	http.HandleFunc("/endpoint", h2)

	log.Fatal(http.ListenAndServe(":8080", nil))
}

これは動作が読みやすいですね,
先程のServeで,エンドポイントに合わせてh1ないしh2が呼ばれるわけです.

Handler ... responceに構造体(struct)を使う

Handlerを使う場合,ServeHttpを構造体内部のメソッドに設定し,構造体を返してresponse作成を行います.
これも公式サンプルを見てみましょう.

package main

import (
	"fmt"
	"log"
	"net/http"
	"sync"
)

type countHandler struct {
	mu sync.Mutex // guards n
	n  int
}

func (h *countHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	h.mu.Lock()
	defer h.mu.Unlock()
	h.n++
	fmt.Fprintf(w, "count is %d\n", h.n)
}

func main() {
	http.Handle("/count", new(countHandler))
	log.Fatal(http.ListenAndServe(":8080", nil))
}

こちらは構造体のメソッドとして渡している分,記述がちょっと複雑になってしまいますが,構造体のパラメータを用いたステートフルな返信ができるという違いがあります.

HandleとHandleFunc ... 登録メソッド

HandleとHandleFuncは`ServeHttp'が実装された構造体/ServerHttpそのものの関数をHandlerに「登録」するメソッドです.

ちなみにhttp.Handlerやhttp.HandlerFuncはDefaultServeMux.Handleを内部的に呼び出す糖衣構文のようです.

// https://golang.org/src/net/http/server.go?s=73173:73217#L2391
func Handle(pattern string, handler Handler) { DefaultServeMux.Handle(pattern, handler) }

僕「...あれっ?このサンプル,インタフェースに必要なServeHTTPが実装されてないやん!」

先程のサンプルをもう一度見てみましょう.

package main

import (
	"fmt"
	"log"
	"net/http"
	"sync"
)

type countHandler struct {
	mu sync.Mutex // guards n
	n  int
}

func (h *countHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	h.mu.Lock()
	defer h.mu.Unlock()
	h.n++
	fmt.Fprintf(w, "count is %d\n", h.n)
}

func main() {
	http.Handle("/count", new(countHandler))
	log.Fatal(http.ListenAndServe(":8080", nil))
}

http.Handleメソッドの引数として渡している構造体countHandlerにはserveHTTPが定義されていません.確かにserveHTTPは定義されていますが,よく見ると構造体内部ではなく,main.goの「地」で定義されている用に見えます.
ということでやっぱり,countHandlerにはserveHTTPメソッドがないので,Handlerとして解釈できません.
あれ???

これについては,teratail先駆者様のブログに答えがありました.

Goではあらゆる非インターフェース型の定義に対しメソッドを追加することができます。

こういうコードを想定。特に最初???だったのはここ
func (h *AppHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Println(h.appName)
}
これは
type AppHandler struct {
appName string
}
の構造体に ServeHTTP メソッドを定義している。

なるほど...
Golangはクラスがないこと,また構造体に後からメソッドを追加することができるのを忘れていました.つまり,func (h *countHandler) ServeHTTPとすることで,構造体にserveHTTPを登録できるので,これでHandlerインタフェースを継承した型として解釈できる,という具合ですね.
普段,ガチガチにオブジェクト指向寄りなJavaを書いているプログラマなので,これは衝撃的です.
詳しい文法仕様は先駆者様のブログが詳しいので,そちらにまかせます.

まとめ

サンプルソースコードを写経するごとにこうやって掘り下げてると,全然ソースコードが進みませんね...
調べたり記事にしたりは労力がいるので大変ですが,一番勉強になると信じて,頑張って続けたいと思います.

もっとわかりやすくて詳しい内容が知りたい人へ

まずは,go公式のソースコード・リファレンスを見ていただくのが一番正確で確実です.
また,先駆者様の解説記事・ブログがいっぱいあります,そちらをご覧ください.
以下にそれらの情報をまとめておきます.

net/httpのソースコード

https://golang.org/src/net/http/server.go
今回考察したnet/httpのソースコード.いわば総本山です.

net/http/の公式リファレンス

https://pkg.go.dev/net/http
公式の英語解説です.簡潔な説明が載っているほか,サンプルが多数掲載されていてよいですね.

【Go】net/httpパッケージを読んでhttp.HandleFuncが実行される仕組み

https://qiita.com/immrshc/items/1d1c64d05f7e72e31a98
日本語記事で,わかりやすく,かつ詳しく,今回紹介した流れの全体を抑えてくださっています.

Go 言語の http パッケージにある Handle とか Handler とか HandleFunc とか HandlerFunc とかよくわからないままとりあえずイディオムとして使ってたのでちゃんと理解したメモ

https://qiita.com/nirasan/items/2160be0a1d1c7ccb5e65
タイトルの4つのメソッド・構造体について,簡潔にわかりやすく解説されています.

Discussion