目录

Go 1.22 标准库路由实战:用 net/http 构建 REST API

Go 1.22 之前,标准库 net/http 的路由能力非常基础——不支持方法匹配(GET POST),不支持路径参数(/users/{id})。社区因此催生了 ginchiecho 等第三方路由库。

Go 1.22 改变了这一切:标准库现在原生支持方法前缀、路径参数、通配符和后缀匹配。对大多数项目来说,第三方路由库不再是必需品。

新特性速览

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
mux := http.NewServeMux()

// 方法匹配:只有 GET 请求会匹配
mux.HandleFunc("GET /api/users", listUsers)

// 路径参数:用 {name} 捕获路径段
mux.HandleFunc("GET /api/users/{id}", getUser)

// 通配符匹配剩余路径
mux.HandleFunc("GET /api/static/{path...}", serveStatic)

💡 旧写法 mux.HandleFunc("/api/users", ...) 仍有效,但新模式中模式字符串必须包含方法,否则会按旧语义匹配所有 HTTP 方法。

实战:完整 CRUD API

以下是一个用户管理的 REST API——只依赖标准库,零第三方依赖。

 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
56
57
58
59
60
61
62
63
64
65
66
67
68
package main

import (
	"encoding/json"
	"log/slog"
	"net/http"
	"sync"
	"time"
)

type User struct {
	ID        int       `json:"id"`
	Name      string    `json:"name"`
	Email     string    `json:"email"`
	CreatedAt time.Time `json:"created_at"`
}

type UserStore struct {
	mu     sync.RWMutex
	users  map[int]User
	nextID int
}

func NewUserStore() *UserStore {
	return &UserStore{users: make(map[int]User), nextID: 1}
}

func (s *UserStore) List() []User {
	s.mu.RLock()
	defer s.mu.RUnlock()
	result := make([]User, 0, len(s.users))
	for _, u := range s.users {
		result = append(result, u)
	}
	return result
}

func (s *UserStore) Get(id int) (User, bool) {
	s.mu.RLock()
	defer s.mu.RUnlock()
	u, ok := s.users[id]
	return u, ok
}

func (s *UserStore) Create(name, email string) User {
	s.mu.Lock()
	defer s.mu.Unlock()
	u := User{
		ID: s.nextID, Name: name, Email: email,
		CreatedAt: time.Now(),
	}
	s.users[u.ID] = u
	s.nextID++
	return u
}

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

	mux.HandleFunc("GET /api/users", handleListUsers(store))
	mux.HandleFunc("POST /api/users", handleCreateUser(store))
	mux.HandleFunc("GET /api/users/{id}", handleGetUser(store))

	handler := withLogging(withRecovery(mux))
	slog.Info("Server starting on :8080")
	http.ListenAndServe(":8080", handler)
}

路由处理函数

 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
func handleListUsers(store *UserStore) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		users := store.List()
		writeJSON(w, http.StatusOK, users)
	}
}

func handleCreateUser(store *UserStore) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		var req struct {
			Name  string `json:"name"`
			Email string `json:"email"`
		}
		if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
			writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"})
			return
		}
		if req.Name == "" || req.Email == "" {
			writeJSON(w, http.StatusBadRequest, map[string]string{"error": "name and email required"})
			return
		}
		user := store.Create(req.Name, req.Email)
		writeJSON(w, http.StatusCreated, user)
	}
}

func handleGetUser(store *UserStore) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		idStr := r.PathValue("id")
		id, err := parseInt(idStr)
		if err != nil {
			writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid id"})
			return
		}
		user, ok := store.Get(id)
		if !ok {
			writeJSON(w, http.StatusNotFound, map[string]string{"error": "not found"})
			return
		}
		writeJSON(w, http.StatusOK, user)
	}
}

辅助函数与中间件

 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
func writeJSON(w http.ResponseWriter, status int, data any) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(status)
	json.NewEncoder(w).Encode(data)
}

func parseInt(s string) (int, error) {
	var n int
	for _, c := range s {
		if c < '0' || c > '9' {
			return 0, &strconv.NumError{Func: "ParseInt", Num: s, Err: strconv.ErrSyntax}
		}
		n = n*10 + int(c-'0')
	}
	return n, nil
}

func withLogging(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		start := time.Now()
		next.ServeHTTP(w, r)
		slog.Info("request", "method", r.Method, "path", r.URL.Path, "duration", time.Since(start))
	})
}

func withRecovery(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		defer func() {
			if err := recover(); err != nil {
				slog.Error("panic", "error", err)
				http.Error(w, "Internal Server Error", http.StatusInternalServerError)
			}
		}()
		next.ServeHTTP(w, r)
	})
}

验证

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# 创建用户
curl -X POST http://localhost:8080/api/users \
  -H "Content-Type: application/json" \
  -d '{"name":"张三","email":"[email protected]"}'

# 列出用户
curl http://localhost:8080/api/users

# 获取单个用户
curl http://localhost:8080/api/users/1

是否该告别 gin/chi?

场景 推荐方案
小型 API / 微服务 标准库足够 ✅
复杂路由(正则、自定义匹配器) 仍需要 chi / httprouter
超大项目(100+ 路由) gin 的 group 和 binding 更省事

建议:新项目先用标准库。当 HandleFunc 不够用、路由数量膨胀时再引入第三方库。Go 标准库的设计哲学是足够好——对 80% 的项目来说,它确实够了。

踩坑提醒

  1. r.URL.Query().Get("id") 不会匹配 {id}——路径参数必须用 r.PathValue("id")
  2. 模式中的方法不可省略——GET /api/users/api/users 是两种语义
  3. 模式匹配取最具体者——/users/new/users/{id} 优先级更高
  4. Go 版本要求——该功能在 Go 1.22 引入,go.mod 中需声明 go 1.22
1
2
3
// go.mod
module myapi
go 1.22

Go 团队在持续减少标准库的短板。如果你还没升级到 Go 1.22+,这就是最好的理由。