3.5 net/http 與 handler 設計
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 讓服務輸出可搜尋、可關聯的結構化資訊。