Go 的 net/http 把 HTTP endpoint 簡化成一個核心模型:handler 接收 request,然後寫出 response。後端服務可以有複雜的資料庫、queue、背景工作或即時連線,但 HTTP 入口本身應該先保持清楚。

handler 是 HTTP 邊界

HTTP handler 的核心責任是處理協定邊界。它應該讀取 request、驗證輸入、呼叫內部邏輯,最後寫出 status code、header 與 body。

 1func handleHealth(w http.ResponseWriter, r *http.Request) {
 2    if r.Method != http.MethodGet {
 3        http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
 4        return
 5    }
 6
 7    w.Header().Set("Content-Type", "application/json")
 8    w.WriteHeader(http.StatusOK)
 9    fmt.Fprint(w, `{"status":"ok"}`)
10}

這個 handler 只處理健康檢查:確認 HTTP method,設定回應格式,寫出 JSON。它沒有讀取資料庫,也沒有啟動背景工作,因為健康檢查的責任就是讓呼叫者知道服務是否能回應。

handler 可以呼叫內部服務,但不應該把所有業務規則都塞在 HTTP 層。HTTP 層越薄,測試越容易,未來改成 CLI、queue consumer 或其他入口時也比較不會重寫核心邏輯。

http.HandlerFunc 是函式轉接器

http.HandlerFunc 的核心意義是讓普通函式符合 http.Handler 介面。只要函式形狀是 func(http.ResponseWriter, *http.Request),就能成為 HTTP handler。

1func hello(w http.ResponseWriter, r *http.Request) {
2    fmt.Fprint(w, "hello")
3}
4
5func main() {
6    http.HandleFunc("/hello", hello)
7    http.ListenAndServe(":8080", nil)
8}

http.HandleFunc 會把 hello 轉成 handler 並註冊到預設 mux。小範例可以這樣寫,但實際應用通常會建立自己的 ServeMux,避免全域註冊讓測試與組裝變得不清楚。

1func newRouter() http.Handler {
2    mux := http.NewServeMux()
3    mux.HandleFunc("/health", handleHealth)
4    mux.HandleFunc("/users", handleUsers)
5    return mux
6}

回傳 http.Handler 可以隱藏路由實作,呼叫端只需要知道這是一個可被 server 使用的 handler。

ServeMux 負責路由分派

ServeMux 的核心責任是把 request path 對應到 handler。標準庫的 http.NewServeMux 足以建立許多小型服務與教學範例。

 1func main() {
 2    mux := http.NewServeMux()
 3    mux.HandleFunc("GET /health", handleHealth)
 4    mux.HandleFunc("POST /users", handleCreateUser)
 5
 6    server := &http.Server{
 7        Addr:    ":8080",
 8        Handler: mux,
 9    }
10
11    if err := server.ListenAndServe(); err != nil {
12        log.Fatal(err)
13    }
14}

新版 Go 的 ServeMux 支援在 pattern 裡寫 HTTP method,例如 GET /health。這能讓 method 與 path 在註冊處一起呈現。

若你的專案需要 middleware group、path parameter 或更完整的路由功能,可以使用第三方 router。入門階段先理解標準庫模型,會更容易看懂任何 router 的抽象。

request 讀取要有明確限制

讀取 request 的核心原則是只接受你預期的內容。handler 應該檢查 method、content type、body 大小與 JSON 格式,避免把任意輸入直接交給內部邏輯。

 1type createUserRequest struct {
 2    Name  string `json:"name"`
 3    Email string `json:"email"`
 4}
 5
 6func handleCreateUser(w http.ResponseWriter, r *http.Request) {
 7    if r.Method != http.MethodPost {
 8        http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
 9        return
10    }
11
12    defer r.Body.Close()
13    r.Body = http.MaxBytesReader(w, r.Body, 1<<20)
14
15    var req createUserRequest
16    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
17        http.Error(w, "invalid json", http.StatusBadRequest)
18        return
19    }
20
21    if req.Email == "" {
22        http.Error(w, "email is required", http.StatusBadRequest)
23        return
24    }
25
26    w.WriteHeader(http.StatusCreated)
27}

http.MaxBytesReader 限制 body 大小,避免大型輸入消耗過多記憶體。json.Decoder 解析 body,失敗時回傳 400 Bad Request。欄位驗證通過後,handler 才進入真正的建立流程。

這段範例省略了資料儲存,因為本章重點是 HTTP 邊界。實務上通常會把建立使用者的規則放到 service 函式,handler 只負責轉換 request 與 response。

response 要先決定狀態碼

寫 response 的核心規則是先決定 status code,再寫 header 與 body。只要 body 開始寫出,Go 就會送出預設或目前設定的 status code。

1func writeJSON(w http.ResponseWriter, status int, value any) {
2    w.Header().Set("Content-Type", "application/json")
3    w.WriteHeader(status)
4
5    if err := json.NewEncoder(w).Encode(value); err != nil {
6        // response 已經開始寫出,這裡通常只能記錄錯誤。
7        log.Printf("write json response: %v", err)
8    }
9}

WriteHeader 應該在 Encode 之前呼叫。若先寫 body,再呼叫 WriteHeader,狀態碼可能已經固定為 200 OK

1writeJSON(w, http.StatusCreated, map[string]string{
2    "id": "user_123",
3})

小型範例可以直接在 handler 裡寫 response;當多個 handler 都要輸出 JSON 時,抽出 writeJSON 這類 helper 可以減少重複。

handler 可以依賴介面

handler 依賴介面的核心好處是測試與替換更容易。HTTP 層不需要知道資料來自資料庫、記憶體或遠端 API,只需要知道它可以呼叫某個能力。

 1type UserCreator interface {
 2    CreateUser(ctx context.Context, name string, email string) (string, error)
 3}
 4
 5type UserHandler struct {
 6    creator UserCreator
 7}
 8
 9func (h UserHandler) Create(w http.ResponseWriter, r *http.Request) {
10    // 解析與驗證 request 後:
11    id, err := h.creator.CreateUser(r.Context(), "alice", "alice@example.com")
12    if err != nil {
13        http.Error(w, "create user", http.StatusInternalServerError)
14        return
15    }
16
17    writeJSON(w, http.StatusCreated, map[string]string{"id": id})
18}

這裡的 UserHandler 不知道使用者如何被建立,只知道有一個 UserCreator。測試時可以提供假的 creator,正式環境再接上真正實作。

介面不需要一開始就為所有東西建立。當 handler 真的需要隔離外部依賴,或測試需要替換依賴時,再抽出小介面會更自然。

小結

下一章會回到 logging,說明如何用 slog 讓服務輸出可搜尋、可關聯的結構化資訊。