Go の net/http を利用して WEB サーバーをつくる

Go の標準パッケージである net/http を利用して、単純な WEB サーバーを作成してみます。

net/http

環境

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

func http.ListenAndServe

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.

func (*Server) Serve

ただ 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 == "/".

type ServeMux

ちなみに Wikipedia によると、multiplexer(マルチプレクサ)は以下と解説されています。

マルチプレクサ、多重器、多重装置、多重化装置、合波器(multiplexer)は、ふたつ以上の入力をひとつの信号として出力する機構である。通信分野では多重通信の入口の装置、電気・電子回路では複数の電気信号をひとつの信号にする回路である。しばしばMUX等と略される。

Wikipedia - マルチプレクサ

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.

func http.HandleFunc

given pattern に対して、handler function を登録するもの、らしい。

第一引数の string 型の pattern には、URL のパスが入ります。第二引数の関数型の handler には、その URL Path に対応する関数名が入ることになります。つまり、これで各 URL Path に紐づく処理内容を設定するわけですね。

第二引数の、リクエストに対する処理を記述する関数は、ResponseWriterRequest を引数として指定します。

ResponseWriter は、レスポンスを返す際に利用するインターフェースとなります。

type ResponseWriter interface {
    Header() Header
    Write([]byte) (int, error)
    WriteHeader(statusCode int)
}

type ResponseWriter

Request は、受け取った HTTP リクエストの情報を格納する構造体になります。構造体のフィールドには、HTTP Method や Header、Body の情報が含まれます。

type Request

コード

コードを書いてみました。

エンドポイント一覧

以下のエンドポイントを作成しています。

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

air