go mod init 必须指定完整模块路径,如 github.com/yourorg/user-service,不可只写项目名 user-service,否则会导致 import 失败、依赖解析错乱;模块路径应与 Git 仓库地址一致,确保唯一性和可复用性。

go mod init 必须指定完整模块路径,不能只写项目名
很多刚上手的人执行 go mod init user-service,结果生成的 go.mod 里模块名是 user-service,这会导致后续 import 失败、依赖解析错乱,尤其在团队协作或 CI/CD 中直接报错。
正确做法是用 Git 仓库地址作为模块路径,比如 GitHub 项目就该写成:go mod init github.com/yourorg/user-service。这样所有内部包引用(如 github.com/yourorg/user-service/internal/handler)才具备唯一性和可复用性。
- 本地开发阶段也别偷懒用短名,否则一推到 CI 就崩
- 如果还没建远程仓库,先占位用
go mod init example.com/user-service,上线前再 replace -
go list -m能验证当前模块路径是否合法,输出应为完整域名格式
Docker 构建必须分两阶段,否则 Alpine 镜像里跑不起来
Go 程序虽然能静态编译,但默认构建仍可能链接 libc(尤其用了 cgo 的库,比如某些数据库驱动或 DNS 解析逻辑),直接 FROM alpine + COPY 二进制会报 standard_init_linux.go:228: exec user process caused: no such file or directory —— 实际是缺动态库,不是文件不存在。
标准解法是多阶段构建:第一阶段用 golang:1.21-alpine 编译,第二阶段用纯 alpine:latest 或 scratch 运行。
FROM golang:1.21-alpine AS builder WORKDIR /app COPY . . RUN CGO_ENABLED=0 go build -a -ldflags '-extldflags "-static"' -o user-service . <p>FROM alpine:latest RUN apk --no-cache add ca-certificates WORKDIR /root/ COPY --from=builder /app/user-service . CMD ["./user-service"]
-
CGO_ENABLED=0是关键,关掉 cgo 才能真静态链接 - Alpine 需要
ca-certificates,否则 HTTPS 请求(比如调 Consul 或 Prometheus)会失败 - 别用
scratch镜像除非你确认没任何外部依赖,连日志都看不到
http.ListenAndServe 默认不支持健康检查端点,得自己加
Kubernetes 的 livenessProbe 和 readinessProbe 默认走 HTTP GET,但原生 http.ListenAndServe 没内置 /health 或 /readyz。不加的话,K8s 会反复重启 Pod,报 Get "http://x.x.x.x:8080/health": dial tcp x.x.x.x:8080: connect: connection refused。
最简健壮做法是单独起一个监听不同端口的 server,避免主服务阻塞影响业务:
go func() {
http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("ok"))
})
http.ListenAndServe(":8081", nil) // 单独端口,和主服务隔离
}()- 别把健康检查和业务共用一个 mux,一旦路由 panic,整个服务不可用
- 就绪检查(
/readyz)建议加 DB 连接校验,但首次请求前别阻塞主服务启动 - 端口别硬编码,用环境变量或 flag 控制,方便 K8s Service 映射
Gin/Echo 等框架默认不设超时,线上容易堆积 goroutine
用 gin.Default() 或 echo.New() 启动服务,看似简单,但底层 http.Server 的 ReadTimeout、WriteTimeout、IdleTimeout 全是 0(即无限)。遇到慢客户端、网络抖动或恶意长连接,goroutine 会越积越多,最终 OOM。
必须显式配置超时,且三个 timeout 要合理配比:
-
ReadTimeout:从连接建立到读完 request header 的时间,建议 ≤5s -
WriteTimeout:从 response.WriteHeader 到写完 body 的时间,应 ≥业务最长处理时间 -
IdleTimeout:两次 request 之间的空闲时间,通常设为 30–60s
以 Gin 为例,得绕过 engine.Run(),手动构造 http.Server:
srv := &http.Server{
Addr: ":8080",
Handler: r,
ReadTimeout: 5 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 60 * time.Second,
}
log.Fatal(srv.ListenAndServe())真正卡住的地方不在语法,而在每个 timeout 值背后对应的 SLA 和链路水位——比如订单创建接口 P99 是 800ms,那 WriteTimeout 设成 5s 是安全的,但设成 10s 就可能掩盖下游抖动问题。


















