GoでREST APIを作る——net/httpとルーティングの基本

Go標準ライブラリの net/http を中心に、JSON APIの実装・ミドルウェアの組み込み・エラーハンドリングの基本を解説します。

はじめに

GoのREST APIは標準ライブラリだけでも十分に実用的です。本記事では net/http(Go 1.22以降の強化されたルーター)を使い、シンプルなユーザーCRUD APIを実装しながら、エラーハンドリングとミドルウェアの基本を押さえます。


プロジェクトのセットアップ

1
2
mkdir go-api && cd go-api
go mod init github.com/yourname/go-api

ディレクトリ構成:

go-api/
├── main.go
├── handler/
│   └── user.go
├── middleware/
│   └── logging.go
└── model/
    └── user.go

モデルの定義

1
2
3
4
5
6
7
8
// model/user.go
package model

type User struct {
    ID    int    `json:"id"`
    Name  string `json:"name"`
    Email string `json:"email"`
}

ハンドラーの実装

Go 1.22からパスパラメータ({id})が標準ルーターで使えるようになりました。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// handler/user.go
package handler

import (
    "encoding/json"
    "net/http"
    "strconv"

    "github.com/yourname/go-api/model"
)

var users = []model.User{
    {ID: 1, Name: "Alice", Email: "alice@example.com"},
    {ID: 2, Name: "Bob",   Email: "bob@example.com"},
}

func ListUsers(w http.ResponseWriter, r *http.Request) {
    writeJSON(w, http.StatusOK, users)
}

func GetUser(w http.ResponseWriter, r *http.Request) {
    id, err := strconv.Atoi(r.PathValue("id"))
    if err != nil {
        writeError(w, http.StatusBadRequest, "invalid id")
        return
    }
    for _, u := range users {
        if u.ID == id {
            writeJSON(w, http.StatusOK, u)
            return
        }
    }
    writeError(w, http.StatusNotFound, "user not found")
}

func CreateUser(w http.ResponseWriter, r *http.Request) {
    var u model.User
    if err := json.NewDecoder(r.Body).Decode(&u); err != nil {
        writeError(w, http.StatusBadRequest, "invalid request body")
        return
    }
    u.ID = len(users) + 1
    users = append(users, u)
    writeJSON(w, http.StatusCreated, u)
}

func writeJSON(w http.ResponseWriter, status int, v any) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    json.NewEncoder(w).Encode(v)
}

func writeError(w http.ResponseWriter, status int, msg string) {
    writeJSON(w, status, map[string]string{"error": msg})
}

ルーティングとサーバー起動

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// main.go
package main

import (
    "log"
    "net/http"

    "github.com/yourname/go-api/handler"
    "github.com/yourname/go-api/middleware"
)

func main() {
    mux := http.NewServeMux()

    mux.HandleFunc("GET /users",          handler.ListUsers)
    mux.HandleFunc("GET /users/{id}",     handler.GetUser)
    mux.HandleFunc("POST /users",         handler.CreateUser)

    srv := &http.Server{
        Addr:    ":8080",
        Handler: middleware.Logging(mux),
    }

    log.Println("Starting server on :8080")
    log.Fatal(srv.ListenAndServe())
}

Go 1.22では "GET /users" のようにHTTPメソッドをパターンに含められます。以前のバージョンと異なり、GETとPOSTを同じパスで別々のハンドラーに振り分けられます。


ロギングミドルウェア

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// middleware/logging.go
package middleware

import (
    "log"
    "net/http"
    "time"
)

func Logging(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        rw := &responseWriter{ResponseWriter: w, status: http.StatusOK}
        next.ServeHTTP(rw, r)
        log.Printf("%s %s %d %s", r.Method, r.URL.Path, rw.status, time.Since(start))
    })
}

type responseWriter struct {
    http.ResponseWriter
    status int
}

func (rw *responseWriter) WriteHeader(status int) {
    rw.status = status
    rw.ResponseWriter.WriteHeader(status)
}

http.ResponseWriter をラップしてステータスコードをキャプチャするのが定石です。標準の ResponseWriter はステータスコードを後から取り出せないため、このラッパーが必要になります。


動作確認

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
go run .

# 一覧取得
curl http://localhost:8080/users

# 個別取得
curl http://localhost:8080/users/1

# 作成
curl -X POST http://localhost:8080/users \
  -H "Content-Type: application/json" \
  -d '{"name":"Charlie","email":"charlie@example.com"}'

# 存在しないID
curl http://localhost:8080/users/99
# {"error":"user not found"}

次のステップ

この構成をベースにしてよく追加されるのは以下の要素です。

要素手段
DB接続database/sql + pgx
バリデーションgo-playground/validator
認証JWT + Bearerトークン
設定管理os.Getenv または spf13/viper
テストhttptest.NewRecorder でハンドラー単体テスト

まとめ

Go 1.22以降の標準ルーターはメソッドとパスパラメータに対応しており、シンプルなAPIであればサードパーティのルーターなしで実装できます。ミドルウェアは http.Handler を受け取って http.Handler を返す関数として実装するのが慣例です。エラーレスポンスは writeError のようなヘルパーに集約しておくと一貫したフォーマットを保てます。