几行代码搭一个服务器
处理器(handler)是一个函数,它接收请求并写出响应。ServeMux 负责把请求路由到对应的处理器。
编辑器无法接受来自你浏览器的连接,所以本页的示例都用 httptest.NewServer 启动服务器,并在同一个程序里调用它。在真实程序中,最后那部分会换成一行代码,它会阻塞并一直提供服务:
log.Fatal(http.ListenAndServe(":8080", mux))
然后 curl localhost:8080/hello/gopher 会打印 Hello, gopher!。ListenAndServe 只在出错时才返回(比如端口已被占用),所以它被包在 log.Fatal 里。
每个请求都在自己的 goroutine 中运行。处理器之间共享的任何东西,比如一个 map 或一个计数器,都需要一个互斥锁。
处理器
任何带有 ServeHTTP(http.ResponseWriter, *http.Request) 方法的类型都是 http.Handler。http.HandlerFunc 把普通函数适配成这个接口,mux.HandleFunc 会替你完成这个转换。当处理器需要依赖时,用结构体作为处理器会很方便:
type API struct {
db *sql.DB
}
func (a *API) listItems(w http.ResponseWriter, r *http.Request) { /* uses a.db */ }
mux.HandleFunc("GET /items", api.listItems)
*http.Request 为你提供:
| 字段或方法 | 内容 |
|---|---|
r.Method | GET、POST、... |
r.URL.Path | 路径,如 /items/42 |
r.PathValue("id") | 路由模式中的通配符(Go 1.22) |
r.URL.Query().Get("q") | 查询字符串参数 |
r.Header.Get("Authorization") | 请求头 |
r.Body | 请求体,一个 io.ReadCloser(由服务器负责关闭) |
r.FormValue("name") | 表单字段或查询参数 |
r.Context() | 客户端断开连接时会被取消的 context |
路由模式(Go 1.22)
从 Go 1.22 起,ServeMux 的模式形如 [METHOD ][HOST]/[PATH],路径中可以包含通配符。
| 模式 | 匹配 |
|---|---|
"/items/" | /items/ 及其下的所有路径(末尾斜杠 = 前缀) |
"/items" | 只匹配 /items |
"GET /items/{id}" | /items/42 上的 GET(以及 HEAD);r.PathValue("id") == "42" |
"POST /items" | 只匹配 /items 上的 POST |
"/files/{path...}" | /files/a/b/c;path 为 "a/b/c" |
"/{$}" | 只匹配 /,而不是所有路径 |
"/" | 其他模式都不匹配的所有路径 |
当两个模式都匹配时,更具体的那个胜出,所以 /items/new 优先于 /items/{id}。如果两者都不比对方更具体,比如 /items/{id} 和 /{kind}/new(两者都匹配 /items/new),注册第二个时就会 panic,报错信息会写出这两个模式。如果路径匹配但方法不匹配,mux 会自动返回 405 Method Not Allowed,并附上 Allow 响应头,你不用写任何代码。
"/" 模式会匹配所有路径。有人用 "/" 注册首页,结果发现它对每个未知 URL 都返回 200,这常常让人意外。首页请用 "GET /{$}"。
这些模式要求 go.mod 中写的是 go 1.22 或更高版本。如果版本行更旧,mux 会退回到旧的行为:它把 "GET /items" 当成一个主机名加一个路径,于是这条路由永远不会匹配,所有发往 /items 的请求都会得到 404。
一个小型 JSON API
最后一个请求用的是 DELETE,没有任何路由接受它,于是 mux 自动返回 405,并带上 Allow: GET, HEAD:这些是为该路径注册过的方法(GET 路由也接受 HEAD)。
状态码与写入顺序
一个响应有三个部分,必须按顺序写入:响应头、状态码、响应体。
w.Header().Set(...)修改响应头。它只在状态码发送之前有效。w.WriteHeader(code)发送状态行和响应头。w.Write(...)(或者fmt.Fprint(w, ...),或者一个编码器)发送响应体。如果还没调用过WriteHeader,第一次Write会先发送200 OK。
由此可知:
- 响应体开始写入之后再设置响应头,不会有任何效果。
- 调用两次
WriteHeader会记录http: superfluous response.WriteHeader call日志,并保留第一次的状态码。 - 返回错误响应之后要
return。http.Error不会终止你的处理器,它之后的代码会继续往同一个响应里写内容。
请使用具名常量(http.StatusOK、http.StatusCreated、http.StatusBadRequest、http.StatusUnauthorized、http.StatusNotFound、http.StatusInternalServerError),而不是直接写数字。http.StatusText(404) 返回 "Not Found"。
中间件
中间件是一个接收处理器并返回处理器的函数,它在调用下一个处理器之前或之后做一些事情:
对同一个请求,log: 这一行总是出现在 client got: 之前。处理器所在的 goroutine 在返回前打印它,而服务器要等处理器返回后才会完成响应。
statusRecorder 嵌入了真正的 ResponseWriter,只重写了 WriteHeader,这是在中间件中获取状态码的标准做法。
生产环境配置
http.ListenAndServe 使用的服务器没有任何超时,所以一个很慢的客户端可以无限期地占用连接。凡是暴露在互联网上的服务,都要显式配置一个 http.Server,并让它优雅地关闭:
srv := &http.Server{
Addr: ":8080",
Handler: mux,
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 10 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 120 * time.Second,
}
go func() {
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatal(err)
}
}()
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
<-ctx.Done() // wait for Ctrl+C or a SIGTERM from the orchestrator
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := srv.Shutdown(shutdownCtx); err != nil { // finish in-flight requests
log.Println("shutdown:", err)
}
Shutdown 会停止接受新连接,并等待正在处理的请求完成,最长等到 context 的截止时间。Shutdown 一开始,ListenAndServe 就会返回 http.ErrServerClosed,所以这个错误不被当作失败。对于运行时间较长的处理器,要把 r.Context() 传下去,这样客户端离开时它们就会停止。
提供静态文件只需要一行:mux.Handle("GET /static/", http.StripPrefix("/static/", http.FileServer(http.Dir("public"))))。
常见错误
- 调用
http.Error之后没有返回。 处理器会继续运行,往响应里写入更多内容。 - 写入响应体之后才设置响应头。 这些响应头会被悄无声息地丢弃。
- 把首页注册在
"/"上。 它会变成所有未知路径的兜底路由。请用"/{$}"。 - 处理器之间共享状态却不加锁。 请求是并发运行的。
- 在生产环境中使用默认服务器。 它没有超时;请在
http.Server上设置超时。 - 路由模式不生效。 如果
"GET /x/{id}"从来不匹配,检查go.mod中是否写的是go 1.22或更高版本。
常见问题
Go 语言怎么创建一个简单的 Web 服务器?
注册一个处理器,然后开始监听:http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "hello") }),接着 log.Fatal(http.ListenAndServe(":8080", nil))。标准库的服务器可以直接用于生产环境;它支持 HTTP/1.1、基于 TLS 的 HTTP/2、keep-alive,并为每个连接分配一个 goroutine。
Go net/http 怎么获取路径参数?
从 Go 1.22 起,ServeMux 的模式可以包含通配符:注册 mux.HandleFunc("GET /items/{id}", h),然后在处理器里用 r.PathValue("id") 读取值。末尾的 {path...} 匹配路径的剩余部分。在 1.22 之前,你得自己拆分 r.URL.Path,或者使用 chi 之类的路由库。
用 Go 写 REST API 需要 Gin 这样的框架吗?
不需要。从 Go 1.22 起,net/http 可以按方法和路径参数路由,而这正是过去人们使用路由库的主要原因。用 encoding/json 处理请求体,再写几个小的中间件函数负责日志和认证,标准库就足以应付大多数 API。框架提供的是请求绑定、校验之类的便利功能。
Go HTTP 处理器中怎么设置状态码?
在写入响应体之前调用 w.WriteHeader(http.StatusCreated)。如果先写了响应体,Go 会自动发送 200 OK,之后的 WriteHeader 会被忽略,并记录一条 superfluous response.WriteHeader call 日志。响应头也要在 WriteHeader 之前用 w.Header().Set 设置;返回错误时,http.Error(w, msg, code) 会把这些全部做好。