在Go语言标准库中,net/http包提供了完整的HTTP客户端与服务端实现。构建Web服务器时,我们主要使用http.ListenAndServe函数配合处理器(Handler)来接收并响应请求。理解请求对象与响应写入器的交互机制,是写出正确服务端代码的前提。

net/http核心组件与请求处理流程
net/http的服务端模型建立在几个关键类型之上:http.Server负责监听和连接管理,http.ServeMux作为多路复用器将URL映射到对应的Handler,而http.Handler接口仅要求实现ServeHTTP(http.ResponseWriter, *http.Request)方法。当TCP连接建立后,Server会启动独立 goroutine 读取请求行与头部,构造出Request对象,再交由匹配到的Handler处理。
默认情况下,http.ListenAndServe(":8080", nil)使用的nil代表默认的ServeMux(http.DefaultServeMux)。我们可以在初始化阶段通过http.HandleFunc注册路径与函数。需要特别注意,Request对象中的字段如Method、URL、Header都是只读或需谨慎修改的,而ResponseWriter是用于向客户端发送数据的唯一通道。一旦调用了WriteHeader或Write,状态码和头部就会被提交,后续再修改将无效。
下面的示例展示了最基础的服务启动方式,以及如何在函数中读取请求方法并写回不同内容:
package main
import (
"fmt"
"net/http"
)
func hello(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
w.WriteHeader(http.StatusMethodNotAllowed)
fmt.Fprintln(w, "仅支持GET")
return
}
fmt.Fprintln(w, "hello world")
}
func main() {
http.HandleFunc("/hello", hello)
http.ListenAndServe(":8080", nil)
}
请求数据的解析与表单处理
在真实业务中,服务器需要从URL查询参数或请求体中获取用户输入。net/http将查询参数暴露在Request.URL.Query()返回的url.Values中,而表单数据则要通过调用ParseForm或ParseMultipartForm来填充Request.Form与Request.PostForm。未调用解析方法直接读取Form会拿到空值,这是初学者经常遇到的坑。
对于JSON格式的请求体,标准库并未提供自动反序列化,我们需要使用encoding/json包手动解码。注意Request.Body是一个io.ReadCloser,读取后应由Handler负责关闭,不过net/http在请求处理结束时会自动关闭它,我们通常在解码出错时直接返回响应即可。下面的代码演示了同时处理查询参数与JSON体的做法:
package main
import (
"encoding/json"
"net/http"
)
type LoginReq struct {
User string `json:"user"`
Pass string `json:"pass"`
}
func login(w http.ResponseWriter, r *http.Request) {
name := r.URL.Query().Get("from")
if name == "" {
name = "default"
}
var req LoginReq
if r.Header.Get("Content-Type") == "application/json" {
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte("无效的JSON"))
return
}
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]string{
"msg": "欢迎 " + req.User + " 来自 " + name,
"code": "0",
})
}
func main() {
http.HandleFunc("/login", login)
http.ListenAndServe(":8080", nil)
}
上述代码先通过查询参数获取来源标识,再依据内容类型解析JSON。使用w.Header().Set设置响应头必须在WriteHeader之前,否则头部设置不会生效。这种显式控制虽然繁琐,但让开发者对HTTP交互拥有完全掌控。
中间件设计与响应复用的最佳实践
随着功能增加,我们往往需要在多个Handler中复用日志、鉴权等逻辑。net/http的中间件本质上是一个接收http.Handler并返回新http.Handler的函数。它在调用下一层之前可以修改Request,在之后可以捕获响应状态。由于ResponseWriter本身不记录状态码,若要统计404数量,需用自定义类型包装它。
一个常见错误是在中间件中调用了Write,却忘了调用下一层Handler,导致请求被短路。正确做法是将包装后的ResponseWriter与原始Request传入next.ServeHTTP。以下示例实现了一个简单的日志中间件,并记录处理耗时:
package main
import (
"log"
"net/http"
"time"
)
type statusWriter struct {
http.ResponseWriter
status int
}
func (s *statusWriter) WriteHeader(code int) {
s.status = code
s.ResponseWriter.WriteHeader(code)
}
func logging(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
sw := &statusWriter{ResponseWriter: w, status: http.StatusOK}
start := time.Now()
next.ServeHTTP(sw, r)
log.Printf("%s %s %d %v", r.Method, r.URL.Path, sw.status, time.Since(start))
})
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("ok"))
})
http.ListenAndServe(":8080", logging(mux))
}
通过自定义ServeMux并包裹logging中间件,我们既保留了标准库路由的简洁,又获得了统一的观测能力。相比直接使用框架,这种写法依赖更少,二进制体积更小,适合对性能和可控性要求较高的内部服务。在掌握net/http的这些机制后,你可以平滑过渡到更复杂的路由库,而底层原理依然适用。