Golang初心者が`net/http`パッケージでWebサーバーをホストする流れを追う
概要
こんにちは.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メソッドが呼ばれる
}
ここで,Handler
がnil
の場合,DefaultServeMux
がHandler
として割り当てられるようです.
DefaultServeMux
については,先程HandlerFunc
の関数が登録される先として登場しました.DefaultServeMux
はServeMux
型のハンドラで,最初からあるデフォルトのハンドラです.
// 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
メソッドが呼ばれます.handler
がnil
の場合,ここで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メソッドで,ユーザーが渡すHandler
やHandlerFunc
のメソッドがようやく呼ばれることになります.長かった......
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のソースコード
今回考察したnet/httpのソースコード.いわば総本山です.
net/http/
の公式リファレンス
公式の英語解説です.簡潔な説明が載っているほか,サンプルが多数掲載されていてよいですね.
【Go】net/httpパッケージを読んでhttp.HandleFuncが実行される仕組み
日本語記事で,わかりやすく,かつ詳しく,今回紹介した流れの全体を抑えてくださっています.
Go 言語の http パッケージにある Handle とか Handler とか HandleFunc とか HandlerFunc とかよくわからないままとりあえずイディオムとして使ってたのでちゃんと理解したメモ
タイトルの4つのメソッド・構造体について,簡潔にわかりやすく解説されています.
Discussion