Go の net/http を利用して WEB サーバーをつくる
Go の標準パッケージである net/http
を利用して、単純な WEB サーバーを作成してみます。
環境
- go 1.19.1
最低限理解したこと
コードを書くにあたり、最低限理解した以下2つの関数についてメモしておきます。
- func http.ListenAndServe
- func http.HandleFunc
とりとめもない、ただのメモになっています。
func http.ListenAndServe
func http.ListenAndServe
について、ドキュメントには以下とあります。
func ListenAndServe(addr string, handler Handler) error
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.
TCP をリッスンして、リクエストを処理するハンドラーと一緒に Serve
を呼ぶもの、とのこと。ソースコードをたどると、Serve
とは下記のメソッドだと分かります。リクエストの都度、ゴールーチンを作っているんですね。
func (srv *Server) Serve(l net.Listener) error
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.
ただ The handler is typically nil, in which case the DefaultServeMux is used.
の DefaultServeMux
なるものが、よく分からない。ソースコードを読むと、DefaultServeMux
は default の ServeMux
らしい。。。
// DefaultServeMux is the default ServeMux used by Serve.
var DefaultServeMux = &defaultServeMux
var defaultServeMux ServeMux
https://cs.opensource.google/go/go/+/refs/tags/go1.19.2:src/net/http/server.go;l=2308
ServeMux
が不明のため、さらに ServeMux
型の説明を読みにいくと、an HTTP request multiplexer
と説明されている。ただ、より詳細な説明を読む限り、いわゆるルーター的なものになるのだろうか。
ServeMux is an HTTP request multiplexer. It matches the URL of each incoming request against a list of registered patterns and calls the handler for the pattern that most closely matches the URL.
Patterns name fixed, rooted paths, like "/favicon.ico", or rooted subtrees, like "/images/" (note the trailing slash). Longer patterns take precedence over shorter ones, so that if there are handlers registered for both "/images/" and "/images/thumbnails/", the latter handler will be called for paths beginning "/images/thumbnails/" and the former will receive requests for any other paths in the "/images/" subtree.
Note that since a pattern ending in a slash names a rooted subtree, the pattern "/" matches all paths not matched by other registered patterns, not just the URL with Path == "/".
ちなみに Wikipedia によると、multiplexer(マルチプレクサ)は以下と解説されています。
マルチプレクサ、多重器、多重装置、多重化装置、合波器(multiplexer)は、ふたつ以上の入力をひとつの信号として出力する機構である。通信分野では多重通信の入口の装置、電気・電子回路では複数の電気信号をひとつの信号にする回路である。しばしばMUX等と略される。
func http.HandleFunc
func http.HandleFunc
について、ドキュメントには以下とあります。
func HandleFunc(pattern string, handler func(ResponseWriter, *Request))
HandleFunc registers the handler function for the given pattern in the DefaultServeMux. The documentation for ServeMux explains how patterns are matched.
given pattern に対して、handler function を登録するもの、らしい。
第一引数の string 型の pattern には、URL のパスが入ります。第二引数の関数型の handler には、その URL Path に対応する関数名が入ることになります。つまり、これで各 URL Path に紐づく処理内容を設定するわけですね。
第二引数の、リクエストに対する処理を記述する関数は、ResponseWriter
と Request
を引数として指定します。
ResponseWriter
は、レスポンスを返す際に利用するインターフェースとなります。
type ResponseWriter interface { Header() Header Write([]byte) (int, error) WriteHeader(statusCode int) }
Request
は、受け取った HTTP リクエストの情報を格納する構造体になります。構造体のフィールドには、HTTP Method や Header、Body の情報が含まれます。
コード
コードを書いてみました。
エンドポイント一覧
以下のエンドポイントを作成しています。
Path | 紐づく handler func | 処理概要 |
---|---|---|
/1 | baseGet | 基本的な GET 処理 |
/2 | headerGet | Header から値を取得する GET 処理 |
/3 | queryGet | Query Paramter から値を取得する GET 処理 |
/4 | basePost | 基本的な POST 処理 |
具体的なコード
package main import ( "encoding/json" "fmt" "log" "net/http" ) // 基本的な GET Method func baseGet(w http.ResponseWriter, req *http.Request) { // http.Request.Method の値で処理分岐 switch req.Method { // HTTP Mehod として定義されている定数 // https://pkg.go.dev/net/http#MethodGet case http.MethodGet: // 指定した HTTP ステータスでレスポンスを返す // https://pkg.go.dev/net/http#ResponseWriter.WriteHeader // HTTP ステータスコード として定義されている定数 // https://pkg.go.dev/net/http#StatusOK w.WriteHeader(http.StatusOK) w.Write([]byte("baseGet.\n")) default: w.WriteHeader(http.StatusMethodNotAllowed) w.Write([]byte("Method not allowed.\n")) } } // Request Header より値を取得する GET Method func headerGet(w http.ResponseWriter, req *http.Request) { switch req.Method { case http.MethodGet: w.WriteHeader(http.StatusOK) // http.Request.Header にヘッダー情報が map 型で入っている // type Header map[string][]string h := req.Header // Header 型の Get メソッドで、指定したヘッダー情報を取得 // https://pkg.go.dev/net/http#Header.Get msg := fmt.Sprintf("headerGet. User-Agent: %s\n", h.Get("User-Agent")) w.Write([]byte(msg)) default: w.WriteHeader(http.StatusMethodNotAllowed) w.Write([]byte("Method not allowed.\n")) } } // Query Parameter を受け取る GET Method func queryGet(w http.ResponseWriter, req *http.Request) { switch req.Method { case http.MethodGet: w.WriteHeader(http.StatusOK) // `func (u *URL) Query() Values` で Query をパース // https://pkg.go.dev/net/url#URL.Query // `func (v Values) Get(key string) string` で指定した key の値を取得 // https://pkg.go.dev/net/url#Values.Get name := req.URL.Query().Get("name") msg := fmt.Sprintf("queryGet. name: %s\n", name) w.Write([]byte(msg)) default: w.WriteHeader(http.StatusMethodNotAllowed) w.Write([]byte("Method not allowed.\n")) } } // POST Method で受け取る body を定義 type bodyJSON struct { Name string `json:"name"` } // 基本的な POST Method func basePost(w http.ResponseWriter, req *http.Request) { switch req.Method { case http.MethodPost: var jsn bodyJSON // 受け取った body の JSON をデコード if err := json.NewDecoder(req.Body).Decode(&jsn); err != nil { // デコードに失敗した場合の処理 // http.Error 関数は、エラーメッセージと一緒にレスポンスを返してくれる // https://pkg.go.dev/net/http#Error http.Error(w, err.Error(), http.StatusInternalServerError) return } w.WriteHeader(http.StatusCreated) msg := fmt.Sprintf("basePost. name: %s\n", jsn.Name) w.Write([]byte(msg)) default: w.WriteHeader(http.StatusMethodNotAllowed) w.Write([]byte("Method not allowed.\n")) } } func main() { http.HandleFunc("/1", baseGet) http.HandleFunc("/2", headerGet) http.HandleFunc("/3", queryGet) http.HandleFunc("/4", basePost) // TCP ポート取得失敗時にログ出力する log.Fatal(http.ListenAndServe(":8080", nil)) }
リクエスト結果
$ curl -i http://localhost:8080/1 HTTP/1.1 200 OK Date: Fri, 07 Oct 2022 03:15:09 GMT Content-Length: 9 Content-Type: text/plain; charset=utf-8 baseGet.
$ curl -i -X POST http://localhost:8080/1 HTTP/1.1 405 Method Not Allowed Date: Fri, 07 Oct 2022 03:15:41 GMT Content-Length: 20 Content-Type: text/plain; charset=utf-8 Method not allowed.
$ curl -i http://localhost:8080/2 HTTP/1.1 200 OK Date: Fri, 07 Oct 2022 03:15:54 GMT Content-Length: 35 Content-Type: text/plain; charset=utf-8 headerGet. User-Agent: curl/7.68.0
$ curl -i http://localhost:8080/3?name=mikochi HTTP/1.1 200 OK Date: Fri, 07 Oct 2022 03:16:15 GMT Content-Length: 24 Content-Type: text/plain; charset=utf-8 queryGet. name: mikochi
$ curl -i -X POST -H "Content-Type: application/json" -d '{"name":"mikochi"}' http://localhost:8080/4 HTTP/1.1 201 Created Date: Fri, 07 Oct 2022 03:16:41 GMT Content-Length: 24 Content-Type: text/plain; charset=utf-8 basePost. name: mikochi
$ curl -i -X POST -H "Content-Type: application/json" -d '{"name":"mikochi", }' http://localhost:8080/4 HTTP/1.1 500 Internal Server Error Content-Type: text/plain; charset=utf-8 X-Content-Type-Options: nosniff Date: Fri, 07 Oct 2022 03:17:04 GMT Content-Length: 65 invalid character '}' looking for beginning of object key string
WEB サーバーのホットリロード
開発中に起動させている WEB サーバーをホットリロードさせたく、標準パッケージ内には該当機能がないようで、外部パッケージの air
というパッケージを利用するのがいいようです。
$ # インストール $ # v1.40.4 $ go install github.com/cosmtrek/air@latest $ export PATH="$PATH:~/go/bin" $ # カレントディレクトリ内を読んで設定ファイル(.air.toml)を作成してくれる $ air init $ # 実行すると、コード編集都度、WEB サーバーをビルドして起動しなおしてくれる $ air