Go 语言写后端接口的入门教程和实战建议
Go 写后端接口的最小可用路径是:用标准库 net/http + encoding/json 起一个单文件服务,跑通 CRUD 之后再决定要不要引入 Gin 这类框架;而决定这个服务能不能上线的,不是路由写得多漂亮,而是超时、数据库连接池、错误处理和优雅关闭这四件事有没有配对。下面按顺序给出可复制的代码和参数。
起步用标准库 net/http 还是 Gin?
结论:接口数量少于 20 个、且不需要自动参数校验时,直接用标准库;超过这个规模或团队多人协作时再换 Gin。
Go 1.22(2024 年 2 月发布)之后,标准库的 ServeMux 已经支持「方法 + 路径变量」的写法,早期必须靠第三方框架才能做的 RESTful 路由现在内置了:
mux := http.NewServeMux()
mux.HandleFunc("GET /users/{id}", getUser) // r.PathValue("id") 取参数
mux.HandleFunc("POST /users", createUser)
初始化项目只要一条命令:go mod init example.com/api。换成 Gin 的话是 go get github.com/gin-gonic/gin,路由写成 r.POST("/users", createUser),参数绑定用 c.ShouldBindJSON(&req),它能顺手完成必填校验。两者性能差距在真实业务里通常被数据库查询盖过,选型按团队熟悉度决定即可。
一个最小可用的接口怎么写?
结论:一个 handler 只做三件事——解析入参、调用业务逻辑、写响应,其余全部下沉到函数外。
type createUserReq struct {
Name string `json:"name"`
}
func createUser(w http.ResponseWriter, r *http.Request) {
var req createUserReq
dec := json.NewDecoder(r.Body)
dec.DisallowUnknownFields() // 拒绝多余字段,避免拼错参数被静默忽略
if err := dec.Decode(&req); err != nil {
writeErr(w, http.StatusBadRequest, "invalid json")
return
}
if req.Name == "" {
writeErr(w, http.StatusBadRequest, "name required")
return
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(map[string]any{"id": 1, "name": req.Name})
}
注意两个顺序问题:w.Header().Set 必须在 w.WriteHeader 之前调用,否则响应头已经被发送、设置无效;json.NewEncoder(w).Encode 会自己写 200,所以想返回 201 就必须先显式 WriteHeader。
参数解析和统一响应有哪些约定?
结论:入参用 json.Decoder.Decode 而不是 io.ReadAll + json.Unmarshal,出参统一包一层 {code, msg, data}。
Decode 是流式的,不必把整个 body 读进内存,配合 http.MaxBytesReader(w, r.Body, 1<<20) 可以限制请求体最大 1 MB,防止超大 body 打爆内存。统一响应结构的好处是前端只需判断一个字段,示例:
type resp struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data any `json:"data,omitempty"`
}
超时、连接池、优雅关闭怎么配?
结论:这三项不配,服务在压测或发布时必然出问题,且都有明确推荐值。
HTTP 服务端超时:
srv := &http.Server{
Addr: ":8080",
Handler: mux,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 60 * time.Second,
}
database/sql 连接池(官方文档推荐量级):db.SetMaxOpenConns(25)、db.SetMaxIdleConns(25)、db.SetConnMaxLifetime(5 * time.Minute)。不设 ConnMaxLifetime 时,连接可能被 MySQL 的 wait_timeout(默认 28800 秒)单方面掐断,表现为偶发 invalid connection。
调用下游要带上下文超时:ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second),defer cancel()。基于 r.Context() 派生,客户端断开时下游请求会被一起取消。
优雅关闭用 signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) 捕获信号,再调 srv.Shutdown(ctx),它会给正在处理的请求留出时间,不会把响应写一半就断连。
错误处理和并发最容易踩哪些坑?
结论:handler 里不要 panic,跨 goroutine 共享的 map 一定要加锁,错误要能沿调用链传递。
用 fmt.Errorf("query user: %w", err) 包装错误,上层用 errors.Is / errors.As 判断,这样既能加日志上下文,又不丢失原始错误类型。net/http 虽然会 recover handler 里的 panic,但结果是连接被直接关闭、客户端收到空响应,所以业务错误一律走返回值。
并发方面:Go 的 map 不是并发安全的,多 goroutine 同时读写会直接触发运行时 fatal error 而非普通 panic;需要并发访问时用 sync.RWMutex 或 sync.Map。启动的 goroutine 必须能通过 context 退出,否则请求量一大就会泄漏。
怎么写测试并部署上线?
结论:接口测试用 httptest,构建时关闭 CGO 并裁剪符号表。
测试直接构造请求打到 handler,不需要真的监听端口:
req := httptest.NewRequest("POST", "/users", strings.NewReader(`{"name":"a"}`))
w := httptest.NewRecorder()
createUser(w, req)
if w.Code != http.StatusCreated { t.Fatalf("got %d", w.Code) }
跑 go test ./... 加 go vet ./... 作为提交前检查。交叉编译 Linux 二进制:CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-s -w" -o api,产物是单个静态文件,直接丢进容器或 systemd 即可运行。
把上面的要点串起来:先按最小结构跑通接口,再补上 5 秒读超时、25 个连接上限、%w 错误链和 srv.Shutdown 这四项,你的 Go 后端就从「能跑」进入「能上线」了。
原文链接:https://www.gj0.com/thread-106.html
转载请注明出处并保留本声明;内容仅代表作者观点,与本站立场无关。若本文涉嫌侵权,请联系本站处理。