Goのnet/httpで最低限のWeb APIを作成する

6 min read読了の目安(約6100字

はじめに

この記事はGoのnet/httpパッケージで最低限の機能を実装したWeb APIを作成する方法を書きます。実装する機能としては以下のようになります。

  • ローカルでサーバを起動する
  • 単純なルーティング
  • リクエストメソッドを確認する
  • JSON形式でリクエストを受け取り、JSON形式でレスポンスを返す
  • ハンドラチェインでハンドラ関数の共通処理を切り出す

この記事が他の人の参考になれば幸いです。
また、この記事の内容に間違った記載がありましたら、指摘していただけるとありがたいです。

環境

名前 バージョン
macOS Big Sur 11.2.3
Go 1.16

ローカルでサーバを起動する

デフォルトのサーバとマルチプレクサを利用して、ローカルでサーバを起動します。
Goではハンドラがリクエストを受け取り、レスポンスを返します。
マルチプレクサもハンドラの一種であり、URLに応じて登録しているハンドラにリクエストを転送し、ルーティングを行います。

server.go
package main

import (
    "log"
    "net/http"
)

// ハンドラ関数の定義
func getHello(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("Hello, World!"))
}

func main() {
    // デフォルトのマルチプレクサにハンドラ関数を登録
    http.HandleFunc("/", getHello)

    // ポートを開く
    if err := http.ListenAndServe(":8080", nil); err != nil {
        log.Fatal(err)
    }
}
# サーバを起動する
$ go run server.go
# リクエストを送り、レスポンスを確認する
$ curl -i -X GET "http://localhost:8080/"
HTTP/1.1 200 OK
Date: Thu, 18 Mar 2021 08:44:08 GMT
Content-Length: 13
Content-Type: text/plain; charset=utf-8

Hello, World!

http.ListenAndServe関数はデフォルトのサーバを利用し、ポートを開き、リクエストを受け付けます。http.ListenAndServe関数の第1引数にアドレス、第2引数にハンドラを受け取ります。
第2引数がnilのときデフォルトのマルチプレクサであるDefaultServeMuxを使用します。

http.HandleFunc関数でDefaultServeMuxに対応するURLとハンドラ関数を登録しています。
ハンドラ関数のシグネチャは必ずfunc someHanlderFunc(w http.ResponseWriter, r *http.Request)でないといけません。

単純なルーティングを実装する

リクエストのURLによって実行するハンドラを変更します。

...
func getGoodMorning(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("Good Morning!"))
}

func main() {
    http.HandleFunc("/", getHello)
    http.HandleFunc("/morning", getGoodMorning)

    if err := http.ListenAndServe(":8080", nil); err != nil {
        log.Fatal(err)
    }
}
# サーバを起動する
$ go run server.go
$ curl -X GET "http://localhost:8080/"
Hello, World!
$ curl -X GET "http://localhost:8080/morning"
Good Morning!

http.HandleFunc関数で登録するパターンとハンドラ関数によって、URLに対してのハンドラを変更できます。

ここで注意することが一つだけあります。
以下の例でURLに対して呼ばれるハンドラに注目してください。

$ curl -X GET "http://localhost:8080/morning/hoge"
Hello, World!

パターンが//morningがデフォルトのマルチプレクサに登録してある時、URLが/morning/hogeに対して対応するパターンが/になります。
/morning/hogeに対応するハンドラを/morningの方と同じにしたい場合は、/morning/morning/に変更してください。
//morning/の場合/morning/hogeに対応するパターンは/morning/になります。

...
func main() {
    http.HandleFunc("/", getHello)
    http.HandleFunc("/morning/", getGoodMorning)

    if err := http.ListenAndServe(":8080", nil); err != nil {
        log.Fatal(err)
    }
}
$ curl -X GET "http://localhost:8080/morning/hoge"
Good Morning!

リクエストメソッドを確認する

リクエストメソッドを確認して、不適切な場合は405 Method Not Allowedでレスポンスを返します。

...
func getHello(w http.ResponseWriter, r *http.Request) {
    if r.Method != http.MethodGet {
        w.WriteHeader(http.StatusMethodNotAllowed)
        return
    }
    w.Write([]byte("Hello, World!"))
}
...
$ curl -i -X POST "http://localhost:8080/"
HTTP/1.1 405 Method Not Allowed
...

r.Methodでリクエストのリクエストメソッドを取得できます。
http.MethodGetnet/httpパッケージで定義されている定数です。
w.WriteHeader関数はレスポンスのステータスコードを指定できます。レスポンスのヘッダに書き込む関数ではないので注意してください。
http.StatusMethodNotAllowednet/httpパッケージで定義されている定数です。

JSONを受け取り、JSONを返す

JSON形式でリクエストを受け取り、JSON形式でレスポンスを返します。
下のサンプルでは{name: "minguu", times: 3}のようなリクエストを受け取り、{hellos: ["Hello minguu", ...]}のようなレスポンスを返します。

...
type PostHelloRequest struct {
    Name  string `json:"name"`
    Times int    `json:"times"`
}

type PostHelloResponse struct {
    Hellos []string `json:"hellos"`
}

func postHello(w http.ResponseWriter, r *http.Request) {
    if r.Method != http.MethodPost {
        w.WriteHeader(http.StatusMethodNotAllowed)
        return
    }

    var reqBody PostHelloRequest
    if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil {
        w.WriteHeader(http.StatusBadRequest)
        return
    }
    name, times := reqBody.Name, reqBody.Times

    hellos := make([]string, 0, times)
    for i := 0; i < times; i++ {
        hellos = append(hellos, "Hello "+name)
    }

    resBody := PostHelloResponse{hellos}
    encoder := json.NewEncoder(w)
    encoder.SetIndent("", "  ")
    w.Header().Set("Content-Type", "application/json")
    if err := encoder.Encode(resBody); err != nil {
        w.WriteHeader(http.StatusInternalServerError)
        return
    }
}
...
$ curl -i -X POST "http://localhost:8080/" -H  "accept: application/json" -H  "Content-Type: application/json" -d "{  \"name\": \"minguu\", \"times\": 3}"
HTTP/1.1 200 OK
Content-Type: application/json
...

{
  "hellos": [
    "Hello minguu",
    "Hello minguu",
    "Hello minguu"
  ]
}

json形式のリクエストボディとjson形式のレスポンスボディを構造体PostHelloRequestPostHelloResponseで定義しています。
jsonのエンコードとデコードはEncoderDecoderを使用しています。
人にも読みやすいようにencoder.SetIndent関数でレスポンスのJSONにインデントを追加しています。
w.Header()はレスポンスのヘッダを表すHeader型の値を返します。
Header型のSetメソッドを用いてレスポンスのヘッダにContent-Type: application/jsonを設定しています。

ハンドラチェインでハンドラ関数の共通処理を切り出す

開始・終了ログやレスポンスタイムを図る処理などハンドラに共通して行う処理をハンドラチェインを用いて切り出します。

...
func logging(h http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        log.Printf("INFO START %v requeest to %v\n", r.Method, r.URL)
        h(w, r)
        log.Printf("INFO END %v request to %v\n", r.Method, r.URL)
    }
}

func main() {
    http.HandleFunc("/", logging(postHello))
...
}

logging関数はハンドラ関数の型であるhttp.HandlerFuncを受け取り、http.HandlerFuncを返す関数です。ハンドラ関数をラップし、開始・終了ログを出します。
http.HandleFuncでハンドラ関数を登録するときに呼び出し、ハンドラの共通処理を切り出せます。

参考