Go 小工具的配置读取:默认值、缺失字段和错误提示
配置出错时最难受的情况是:程序启动了,跑了十几分钟,在第四步突然报一句 key not found。这时候你已经不知道是配置文件写错了,还是别的地方有问题。这篇记几个在读配置阶段就该处理掉的事情。
一、默认值写在结构体上,不要散在代码里
常见写法是每个用到配置的地方自己兜底:
timeout := conf.Timeout
if timeout == 0 {
timeout = 20
}
问题是一旦有三处用到它,这段判断就会出现三次,哪天默认值要改成 30 秒,就要改三个地方,还容易漏。
更稳的做法是在读配置的函数里一次性补全,之后全程序都当成「一定有名值」来用:
func loadConfig(path string) (config, error) {
var c config
// ... 读取和解析
if c.TimeoutSecs <= 0 {
c.TimeoutSecs = 20
}
if c.Retry < 0 {
c.Retry = 0
}
return c, nil
}
这样默认值只有一处,调用方拿到的一定是补全过的结构。
二、区分「没填」和「填了空值」
Go 的结构体字段零值有个天然缺陷:0、""、false 分不清是用户没写还是显式填的。对有些字段这不重要,对另一些很要命。
比如「并发数」:没填应该用默认值 4,用户显式写 0 可能是想说「不要并发,串行跑」。这两种情况如果都按 0 处理,后者就被悄悄改成了 4。
解决办法有两个。简单的是用指针:
type config struct {
Workers *int `json:"workers"`
}
nil 表示没填,*c.Workers == 0 表示用户明确要 0。麻烦一点但更清楚的是自定义类型,配一个 UnmarshalJSON。
我的经验是:只有真正需要区分时才用指针,其他字段老老实实用值类型。全用指针会让代码到处是解引用,得不偿失。
三、报错要说清楚:哪个文件、哪个字段、期望什么
json.Unmarshal 的错误信息长这样:
invalid character '}' looking for beginning of object key string
它甚至不告诉你行号。直接把它返回给用户,等于什么都没说。
至少要包两层信息:文件名,和「怎么改」:
if err := json.Unmarshal(b, &c); err != nil {
return c, fmt.Errorf("解析 %s 失败:%w", path, err)
}
对于必填字段,单独检查并说明清楚:
if strings.TrimSpace(c.Cookie) == "" {
return c, errors.New("config.json 的 cookie 不能为空,去浏览器里复制一整行 Cookie 粘进来")
}
「不能为空」是事实,「去哪里拿」才是用户真正需要的。
四、配置文件路径要有优先级
同一份程序,本地开发、定时任务、CI 里跑的可能是同一个二进制,但配置位置不同。固定写死 config.json 迟早出问题。
我的习惯是三级,从高到低:
1. 命令行参数 -config /path/file.json 2. 环境变量 APP_CONFIG 3. 当前目录下的默认文件名
实现起来十几行,flag 的默认值直接取环境变量就行:
defaultPath := os.Getenv("APP_CONFIG")
if defaultPath == "" {
defaultPath = "config.json"
}
cfgPath := flag.String("config", defaultPath, "配置文件路径")
这样定时任务的命令行里显式写路径,本地开发直接跑,两边都不用改代码。
五、示例配置里不要放真东西
仓库里的 config.example.json 是给人照抄的。放进去的每个值都会被复制一遍,包括你没注意到的那个真实令牌。
规矩很简单:示例文件里只写占位说明,真实配置进 .gitignore。
{
"cookie": "把浏览器里那一整行 Cookie 粘到这里",
"timeout_seconds": 20
}
顺便说一句,.gitignore 要在第一次 git add 之前写好。等令牌已经进了提交,删文件是没用的,历史里还在。
六、小结
读配置这段代码通常只有几十行,但它是程序的第一道门槛:这里报错清楚,后面能省掉大量「为什么跑不起来」的沟通。
三件事值得每次都做:默认值集中补全、必填字段给出获取方式、解析失败带上文件名。都不难,难的是想起来。
- 点赞
- 收藏
- 关注作者
评论(0)