Go 语言写后端接口的入门教程和实战建议

chinaz
chinaz 正式会员超兽战士
发布于 2026-10-07 05:50 ·1 浏览 ·0 回复

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 后端就从「能跑」进入「能上线」了。

版权声明:本文来自 GJ论坛《Go 语言写后端接口的入门教程和实战建议》
原文链接:https://www.gj0.com/thread-106.html
转载请注明出处并保留本声明;内容仅代表作者观点,与本站立场无关。若本文涉嫌侵权,请联系本站处理。

全部回复 0

还没有回复,来抢沙发~