viper配置必须绑定结构体并禁用自动环境变量映射,否则易导致多环境静默覆盖;http.Client必须显式设置Timeout及Transport子项超时,避免goroutine泄漏;go mod init须指定完整模块路径,禁止相对导入和手动修改go.mod版本。

生产环境的 Go 服务一旦上线,代码规范和安全实践就不是“锦上添花”,而是防止 goroutine 泄漏、内存暴涨、配置覆盖、依赖污染甚至远程执行的关键防线。真实事故里,90% 的线上故障都源于开发阶段被忽略的细节——比如一个没设 Timeout 的 http.Client,或一个硬编码在 viper.GetString("db.host") 里的配置键。
go mod 初始化必须显式指定完整模块路径
直接运行 go mod init 不加参数,会生成类似 module myapp 或基于当前目录名的模块名,这会导致后续所有 import 路径不一致,CI 构建失败、私有包解析错误、go list 无法识别包结构。
- 正确做法:在项目根目录执行
go mod init example.com/myapp(哪怕只是本地开发,也要模拟最终 import 路径) - 模块路径不能以
./或相对路径开头;若用 GitLab 内部地址gitlab.internal/foo/bar,模块名就必须是gitlab.internal/foo/bar -
go.mod生成后立即运行go mod tidy,它只补全已写入的import,所以务必先写好代码再 tidy - 禁止手动编辑
go.mod中的版本号——升级统一走go get github.com/foo/bar@v1.2.4,否则go.sum校验和缺失,CI 直接拒绝构建
配置加载必须绑定结构体,禁用全局 viper 取值
用 viper.GetString("db.host") 这类方式,会让配置键散落在各处,IDE 无法跳转、字段改名不报错、单元测试难 mock,更危险的是 viper 默认开启环境变量自动映射和大小写不敏感,多环境部署时极易静默覆盖关键配置(比如 DB_HOST 覆盖了 db.host)。
- 正确做法:定义结构体承载配置,用
viper.Unmarshal(&cfg)一次性绑定 - 结构体字段必须带
mapstructuretag,如Host string `mapstructure:"host"` - 禁止在业务逻辑中直接调用
viper.Get*;所有配置访问必须通过该结构体字段 - 环境变量映射需显式关闭:
viper.AutomaticEnv()改为viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))并配合viper.BindEnv("db.host", "DB_HOST")
HTTP 客户端必须设置 Timeout,且不可省略 Transport 配置
没设 Timeout 的 http.Client 是 goroutine 泄漏头号元凶。下游响应慢或卡死时,goroutine 会一直阻塞在 net/http 底层读写,永不释放,几天后堆积数万 goroutine,OOM 崩溃。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
立即学习“go语言免费学习笔记(深入)”;
- 必须设置
Timeout:如&http.Client{Timeout: 5 * time.Second} - 仅设
Timeout不够:它只控制整个请求生命周期,不控制连接建立、TLS 握手、响应头读取等子阶段,应显式配置Transport - 推荐最小安全配置:
client := &http.Client{ Timeout: 5 * time.Second, Transport: &http.Transport{ DialContext: (&net.Dialer{ Timeout: 3 * time.Second, KeepAlive: 30 * time.Second, }).DialContext, TLSHandshakeTimeout: 3 * time.Second, ResponseHeaderTimeout: 3 * time.Second, ExpectContinueTimeout: 1 * time.Second, MaxIdleConns: 100, MaxIdleConnsPerHost: 100, IdleConnTimeout: 30 * time.Second, }, } - 避免复用未配置的默认
http.DefaultClient;所有 HTTP 调用必须使用自定义 client 实例
错误处理必须区分业务错误与系统错误,且不可忽略 error 返回值
Go 的 error 是一等公民,忽略它等于放弃对失败路径的控制。线上日志里大量 nil panic 或空响应,往往只是因为某处漏写了 if err != nil { return err }。
- 业务错误用哨兵错误(如
var ErrNotFound = errors.New("not found"))或自定义类型(实现Error()和Unwrap()),用errors.Is(err, ErrNotFound)判断 - 系统错误(I/O、网络、解码失败等)必须记录日志并触发告警,不能只返回给上游
- 禁止用
panic处理业务异常;panic只用于程序无法继续的致命错误(如初始化失败) - 所有标准库 I/O 操作(
io.Read、json.Unmarshal、database/sql.QueryRow.Scan)的error必须显式检查,哪怕你“确定不会出错”
真正难的不是记住这些条目,而是在写第 100 行业务代码时,依然坚持给 http.Client 配超时、给每个 err 加判断、把配置绑进结构体——因为线上故障从不挑时间,只挑你松懈的那一次。

















