我从 Go 1.0 开始写。十年间经历了 net/http 从「勉强能用」到「大部分场景够了」的转变。

2012 年你用标准库写 REST API,每个路由都要手写正则匹配,路径参数要自己从 URL 里抠,方法判断要写 if r.Method == "GET"。那时候第三方框架不是选择,是必需品。

2026 年的今天,Go 1.22 的路由增强已经出来两年了。很多人还在惯性选框架,没停下来看一眼标准库现在长什么样。

这篇不劝你不用框架,而是算一笔账——加一个框架的真正代价是什么,你买到了什么,你付出的值不值。

Go 1.22 改了什么

2024 年 2 月发布的 Go 1.22 给 net/http 带来了两个关键增强:

1. 方法匹配

mux.HandleFunc("GET /api/users", listUsers)
mux.HandleFunc("POST /api/users", createUser)
mux.HandleFunc("PUT /api/users/{id}", updateUser)
mux.HandleFunc("DELETE /api/users/{id}", deleteUser)

以前你得写 mux.HandleFunc("/api/users", handler),然后在 handler 里自己判断 r.Method,自己从路径里提取 ID。

2. 路径参数

mux.HandleFunc("GET /api/users/{id}", func(w http.ResponseWriter, r *http.Request) {
    id := r.PathValue("id")
    // 直接取到 "id" 的值,不需要 chi.URLParam() 或 gin.Param()
})

注意写法是 {id} 不是 :id 也不是 :id。这是 Go 团队自己造的模式语法——支持通配符 {pathname} 和正则 {id:[0-9]+}

这两条合起来,标准库的 ServeMux 从一个路由器残废变成了一个能用的路由器

对比代码:同样的 CRUD,三种写法

写一个简单的用户 CRUD API,三个版本对比。

标准库(Go 1.22+)

package main

import (
    "encoding/json"
    "log"
    "net/http"
    "strconv"
    "sync"
)

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

var (
    users   = make(map[int]User)
    nextID  = 1
    mu      sync.Mutex
)

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

    mux.HandleFunc("GET /api/users", listUsers)
    mux.HandleFunc("POST /api/users", createUser)
    mux.HandleFunc("GET /api/users/{id}", getUser)
    mux.HandleFunc("PUT /api/users/{id}", updateUser)
    mux.HandleFunc("DELETE /api/users/{id}", deleteUser)

    log.Fatal(http.ListenAndServe(":8080", mux))
}

func listUsers(w http.ResponseWriter, r *http.Request) {
    mu.Lock()
    defer mu.Unlock()
    list := make([]User, 0, len(users))
    for _, u := range users {
        list = append(list, u)
    }
    json.NewEncoder(w).Encode(list)
}

func createUser(w http.ResponseWriter, r *http.Request) {
    var u User
    if err := json.NewDecoder(r.Body).Decode(&u); err != nil {
        http.Error(w, err.Error(), 400)
        return
    }
    mu.Lock()
    u.ID = nextID
    nextID++
    users[u.ID] = u
    mu.Unlock()
    w.WriteHeader(201)
    json.NewEncoder(w).Encode(u)
}

func getUser(w http.ResponseWriter, r *http.Request) {
    id, _ := strconv.Atoi(r.PathValue("id"))
    mu.Lock()
    u, ok := users[id]
    mu.Unlock()
    if !ok {
        http.Error(w, "not found", 404)
        return
    }
    json.NewEncoder(w).Encode(u)
}

func updateUser(w http.ResponseWriter, r *http.Request) {
    id, _ := strconv.Atoi(r.PathValue("id"))
    var u User
    if err := json.NewDecoder(r.Body).Decode(&u); err != nil {
        http.Error(w, err.Error(), 400)
        return
    }
    mu.Lock()
    if _, ok := users[id]; !ok {
        mu.Unlock()
        http.Error(w, "not found", 404)
        return
    }
    u.ID = id
    users[id] = u
    mu.Unlock()
    json.NewEncoder(w).Encode(u)
}

func deleteUser(w http.ResponseWriter, r *http.Request) {
    id, _ := strconv.Atoi(r.PathValue("id"))
    mu.Lock()
    delete(users, id)
    mu.Unlock()
    w.WriteHeader(204)
}

零依赖。go build 出来一个二进制。不香?

Chi

package main

import (
    "encoding/json"
    "log"
    "net/http"
    "strconv"
    "sync"
    "github.com/go-chi/chi/v5"
)

// 结构体代码同上

func main() {
    r := chi.NewRouter()

    r.Get("/api/users", listUsers)
    r.Post("/api/users", createUser)
    r.Get("/api/users/{id}", getUser)
    r.Put("/api/users/{id}", updateUser)
    r.Delete("/api/users/{id}", deleteUser)

    log.Fatal(http.ListenAndServe(":8080", r))
}

// handler 代码基本一样,只是 r.PathValue("id") 换成 chi.URLParam(r, "id")

Chi 的写法更干净一点——.Get() .Post() 链式调用比 mux.HandleFunc("GET ...", ...) 少了一个字符。但本质差异不值一提。

Gin

package main

import (
    "net/http"
    "strconv"
    "sync"
    "github.com/gin-gonic/gin"
)

// 结构体同上

func main() {
    r := gin.Default()

    r.GET("/api/users", listUsers)
    r.POST("/api/users", createUser)
    r.GET("/api/users/:id", getUser)
    r.PUT("/api/users/:id", updateUser)
    r.DELETE("/api/users/:id", deleteUser)

    r.Run(":8080")
}

// Gin 用 c.Param("id") 取路径参数
// 返回值用 c.JSON() 封装

Gin 的标记是 :id 而不是 {id}。它多了 gin.Default()(自带 Logger + Recovery 中间件),c.JSON()c.Param()

看完了,说结论:对 90% 的 CRUD 项目,这三者的开发体验差异,不值得一个 import。

你为框架付出了什么

维度标准库ChiGin
依赖数04 (chi + 3个间接)28+
编译时间基准+4-8%+15-30%
二进制体积基准+0.5MB+3-8MB
CVE 暴露面0(Go 官方跟踪)需监控 Chi 的 CVE需监控 Gin + 28 个间接依赖
学习曲线0(你已经在用标准库)低(chi.URLParam)中(Gin Context 是独立的 API)
升级风险无(跟着 Go 版本走)中(Gin 大版本有 break change)

依赖数不是无意义的信仰问题。 每多一个依赖,你就多一个需要跟踪的安全公告源、多一个 Go module graph 的复杂度、多一个 go mod tidy 上如果维护者发版不规范可能引入的问题。

你获得的东西值不值这个代价?

对大多数项目,不值。

什么时候该用框架

上面我说了"90% 的 CRUD 项目不需要框架"。剩下的 10% 是什么?

  1. 复杂中间件链。标准库的 ServeMux 没有中间件机制。你需要自己写一个包装函数把 http.Handler 包一下。一个中间件两个函数还能忍,五个以上就难看了。Chi 的 r.Use() 链式中间件管理确实干净。

  2. 子路由/路由分组/api/v1//api/v2/ 分开处理,标准库的做法是声明多个 ServeMux 然后组合。Chi 的 r.Route("/api/v1", func(r chi.Router) { ... }) 更直观。

  3. 需要极致的请求生命周期控制。Gin 的 c.Set()/c.Get() 在请求范围内传递上下文比标准库的 context.WithValue 少写几行。但说实话,标准库的 r.Context() 配合 context.WithValue 一样能解决。

  4. 团队已经熟悉某个框架且项目已经有大量代码基于它。这时候强行迁移到标准库是不划算的——框架不是性能瓶颈,CI 时间也不是。

中间的选项

如果你需要中间件管理但不想引入 Gin 这种重型框架,Chi 是最优解:它的贡献是 chi.Router 接口和中间件链管理,路由本身已经和标准库的 ServeMux 趋同。它不改变你的 handler 签名(仍然是 http.Handler 不是 gin.HandlerFunc),这意味着你未来切换到标准库几乎没有成本。

实际上我对项目推荐演化路径是这个:

标准库 (1-5 个 handler) → Chi (5-20 个 handler + 中间件) → 标准库 (架构稳定后重构回来)

没错,做完后考虑重构回标准库。因为 Chi 的价值主要在开发期,部署后它只是一个中间层——你付的依赖税是无意义的。

收尾:选型不是选最强的

Go HTTP 框架选型这块,最大的认知误区是把框架当竞争力。

你的业务逻辑才是竞争力。你用什么路由库,你的用户不关心,你的 API 客户端不关心,你的简历可能在面试时被多看一眼但仅此而已。

Go 1.22 之后的标准库路由,已经不是"是否够用"的问题,而是"你到底需要什么"的问题。如果你说不上来你需要框架的哪个特性,那你就不需要框架。

用标准库写一个 HTTP 服务,go build 出来单个二进制,scp 到服务器,跑起来。没有 go.mod 的 indirect 依赖,没有 vendor 目录,没有 replace 指令。

这种轻盈本身,就是一种竞争力。


Go 1.22 是 2024 年 2 月发布的,我假设你已经在用 >= Go 1.22。如果你还在用 Go 1.21 及以下,标准库路由确实不够用,请用 Chi。然后升级 Go。

Go HTTP Framework Selection: The Standard Library Is Enough

I've been writing Go since 1.0. Over the last decade, net/http went from "barely usable" to "enough for most cases."

In 2012, writing a REST API with the standard library meant writing regex patterns by hand, extracting path parameters manually, and checking r.Method in every handler. Third-party frameworks weren't a choice — they were a necessity.

In 2026, Go 1.22's routing enhancements have been out for two years. Yet many developers still reach for a framework out of habit, without checking what the standard library now offers.

This article won't tell you to never use a framework. It will run the numbers: what a framework actually costs you, what you're buying, and whether it's worth the price.

What Changed in Go 1.22

Go 1.22 (released February 2024) added two key enhancements:

1. Method matching:

mux.HandleFunc("GET /api/users", listUsers)
mux.HandleFunc("POST /api/users", createUser)
mux.HandleFunc("PUT /api/users/{id}", updateUser)

2. Path parameters:

mux.HandleFunc("GET /api/users/{id}", func(w http.ResponseWriter, r *http.Request) {
    id := r.PathValue("id")
})

These two changes transformed ServeMux from a routing cripple to a usable router.

Code Comparison: Same CRUD, Three Ways

Standard Library (Go 1.22+)

mux := http.NewServeMux()
mux.HandleFunc("GET /api/users", listUsers)
mux.HandleFunc("POST /api/users", createUser)
mux.HandleFunc("GET /api/users/{id}", getUser)
mux.HandleFunc("PUT /api/users/{id}", updateUser)
mux.HandleFunc("DELETE /api/users/{id}", deleteUser)
http.ListenAndServe(":8080", mux)

Zero dependencies. One go build produces a single binary.

Chi

r := chi.NewRouter()
r.Get("/api/users", listUsers)
r.Post("/api/users", createUser)
r.Get("/api/users/{id}", getUser)
r.Put("/api/users/{id}", updateUser)
r.Delete("/api/users/{id}", deleteUser)
http.ListenAndServe(":8080", r)

Chi's method-chaining syntax is marginally cleaner. But the functional difference is negligible.

Gin

r := gin.Default()
r.GET("/api/users", listUsers)
r.POST("/api/users", createUser)
r.GET("/api/users/:id", getUser)
r.PUT("/api/users/:id", updateUser)
r.DELETE("/api/users/:id", deleteUser)
r.Run(":8080")

Gin uses :id instead of {id} and adds c.JSON(), c.Param(), and built-in middleware.

Bottom line: for 90% of CRUD projects, the development experience difference between these three is not worth a single import.

What You Pay for a Framework

DimensionstdlibChiGin
Dependencies04 (chi + 3 transitive)28+
Build timebaseline+4-8%+15-30%
Binary sizebaseline+0.5MB+3-8MB
CVE surface0 (Go team tracks)must monitor Chi CVEsmust monitor Gin + 28 transitive deps
Learning curve0 (you already use stdlib)LowMedium (Gin Context is its own API)
Upgrade riskNone (follows Go version)LowMedium (Gin major versions break)

Dependency count isn't an abstract concern. Each dependency adds a CVE source to track, a node in your module graph, and a potential go mod tidy headache when maintainers don't follow semver.

Is what you gain worth this cost?

For most projects: no.

When You Actually Need a Framework

The 10% of cases where a framework makes sense:

  1. Complex middleware chains. ServeMux has no built-in middleware mechanism. You can write wrapper functions for 1-2 middlewares, but beyond 5, Chi's r.Use() chain is genuinely cleaner.

  2. Sub-routing / route groups. /api/v1/ vs /api/v2/ — stdlib requires multiple ServeMux instances. Chi's r.Route("/api/v1", func(r chi.Router) { ... }) is more readable.

  3. Request-scoped context (heavy use). Gin's c.Set()/c.Get() is a few lines shorter than stdlib's context.WithValue. Neither is wrong.

  4. Team already invested. If your codebase uses Gin heavily, migrating to stdlib for purity is a waste of money. A framework isn't your bottleneck.

The Middle Option

Chi is the best middle ground: it adds middleware chain management without changing your handler signatures (still http.Handler). Migrating back to stdlib costs almost nothing.

My recommended evolution path:

stdlib (1-5 handlers) → Chi (5-20 handlers + middleware) → stdlib (refactor back after stabilization)

Build with Chi, then refactor back to stdlib when the architecture is stable. Chi's value is in development — in production it's just a tax.

Close: Selection Isn't About the Strongest

The biggest misconception in Go HTTP framework selection is treating the framework as a competitive advantage.

Your business logic is the competitive advantage. Your users don't care what router you use. Your API clients don't care. Your resume might get a second glance in an interview — that's it.

Since Go 1.22, the standard library's routing isn't a question of "is it enough." It's a question of "what do you actually need." If you can't name the framework feature you need, you don't need the framework.

Write your HTTP service with stdlib. go build a single binary. scp it to your server. Run it. No indirect dependencies. No vendor directory. No replace directives.

That lightness is itself a competitive advantage.


Go 1.22 was released in Feb 2024. If you're still on Go ≤1.21, stdlib routing is genuinely insufficient — use Chi. And upgrade your Go.