🐙

golang 製のhttp サーバは、勝手に並行処理をして同時リクエストに対応してくれている

2023/08/15に公開

はじめに

Go言語を使ったWebサーバの構築は非常に簡単だと言われている
実際、http パッケージを使うだけで簡単にWebサーバを構築することができる

package main

import (
    "fmt"
    "net/http"
)

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Hello, World")
    })
    http.ListenAndServe(":8080", nil)
}

ただ、このコードを見ていると、同時にリクエストが来た場合にどうなるのか気になると思う
一見、何もしていないため、同時にリクエストが来た場合には、処理がブロックされてしまうように見える

今回は、この疑問について調べてみた

コードを辿ってみる

もし、http パッケージが複数のリクエストを同時に処理できるようになっているのであれば、どこかでgoroutine などの並列処理をしているはず。
ということで、コードを辿ってみる

上記のような、サーバを作成するコードにおいて、http パッケージから使用しているのは

  • http.HandleFunc
  • http.ListenAndServe
    の2つである。
    両方ともhttp.Server のメソッドであるため、http.Server のコードを辿ってみる

公式ドキュメントから、それぞれの役割を確認する

http.HandleFunc

HandleFunc registers the handler function for the given pattern in the DefaultServeMux. The documentation for ServeMux explains how patterns are matched.

HandleFuncは、与えられたパターンのハンドラ関数をDefaultServeMuxに登録します。ServeMuxのドキュメントでは、パターンがどのようにマッチングされるかを説明しています。

役割としては、DefaultServeMux にハンドラ関数を登録するということがわかる
実装としても、実際にリクエストを処理していない

http.ListenAndServe

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. Handler is typically nil, in which case the DefaultServeMux is used.

ListenAndServeは、TCPネットワークアドレスaddrでリッスンし、ハンドラを呼び出して、着信接続のリクエストを処理します。受け入れられた接続は、TCPキープアライブを有効にするように構成されています。ハンドラは通常nilです。この場合、DefaultServeMuxが使用されます。

役割として、HandleFunc で定義したパスを使用して、実際のリクエストを処理するということがわかる

ListenAndServe の実装を確認する

ListenAndServe の実装を確認すると、以下のようになっている

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)
}

ListenAndServe は、Serve を呼び出しているだけである
Serve の実装を確認する

func (srv *Server) Serve(l net.Listener) error {
	if fn := testHookServerServe; fn != nil {
		fn(srv, l) // call hook with unwrapped listener
	}

	origListener := l
	l = &onceCloseListener{Listener: l}
	defer l.Close()

	if err := srv.setupHTTP2_Serve(); err != nil {
		return err
	}

	if !srv.trackListener(&l, true) {
		return ErrServerClosed
	}
	defer srv.trackListener(&l, false)

	baseCtx := context.Background()
	if srv.BaseContext != nil {
		baseCtx = srv.BaseContext(origListener)
		if baseCtx == nil {
			panic("BaseContext returned a nil context")
		}
	}

	var tempDelay time.Duration // how long to sleep on accept failure

	ctx := context.WithValue(baseCtx, ServerContextKey, srv)
	for {
		rw, err := l.Accept()
		if err != nil {
			if srv.shuttingDown() {
				return ErrServerClosed
			}
			if ne, ok := err.(net.Error); ok && ne.Temporary() {
				if tempDelay == 0 {
					tempDelay = 5 * time.Millisecond
				} else {
					tempDelay *= 2
				}
				if max := 1 * time.Second; tempDelay > max {
					tempDelay = max
				}
				srv.logf("http: Accept error: %v; retrying in %v", err, tempDelay)
				time.Sleep(tempDelay)
				continue
			}
			return err
		}
		connCtx := ctx
		if cc := srv.ConnContext; cc != nil {
			connCtx = cc(connCtx, rw)
			if connCtx == nil {
				panic("ConnContext returned nil")
			}
		}
		tempDelay = 0
		c := srv.newConn(rw)
		c.setState(c.rwc, StateNew, runHooks) // before Serve can return
		go c.serve(connCtx)
	}
}

Serve は、Accept したリクエストを処理するために、goroutine を起動していることがわかる
コメントにも下記のように記載されており、goroutine を使用した並行処理を行っていることがわかる

// 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.

// Serveは、Listener lの着信接続を受け入れ、それぞれに新しいサービスgoroutineを作成します。サービスgoroutineはリクエストを読み取り、srv.Handlerを呼び出してそれらに応答します。

まとめ

今回は、http パッケージが複数のリクエストを同時に処理できるようになっているのかを調べてみた
結果として、http パッケージは、goroutine を使用して、複数のリクエストを同時に処理できるようになっていることがわかった

GitHubで編集を提案

Discussion