must
must 用于简化线性流程中重复的 if err != nil。Get、OK 系列负责传播 error,Do、Call 系列在边界将其还原为 error。
适合错误策略只是“立即返回”的连续步骤。
环境要求
安装
go get github.com/viocha/must
import "github.com/viocha/must"
为什么使用 must
假设一段逻辑需要依次解析端口、解析超时时间并校验配置,常规写法需要在每一步检查 error:
func loadSettings(rawPort, rawTimeout string) (int, time.Duration, error) {
port, err := strconv.Atoi(rawPort)
if err != nil {
return 0, 0, err
}
timeout, err := time.ParseDuration(rawTimeout)
if err != nil {
return 0, 0, err
}
if err := validateSettings(port, timeout); err != nil {
return 0, 0, err
}
return port, timeout, nil
}
使用 must 后,每个调用只保留业务返回值,错误在 Call2 边界统一返回:
func loadSettings(rawPort, rawTimeout string) (int, time.Duration, error) {
return must.Call2(func() (int, time.Duration) {
port := must.Get(strconv.Atoi(rawPort))
timeout := must.Get(time.ParseDuration(rawTimeout))
must.OK(validateSettings(port, timeout))
return port, timeout
})
}
闭包中连续调用了两次 Get 和一次 OK。任何一步失败都会立即终止后续逻辑,并由 Call2 返回对应的原始 error。
API
func OK(err error)
func Get[T any](v T, err error) T
func Get2[T1, T2 any](v1 T1, v2 T2, err error) (T1, T2)
func Get3[T1, T2, T3 any](v1 T1, v2 T2, v3 T3, err error) (T1, T2, T3)
func Do(fn func()) error
func Call[T any](fn func() T) (T, error)
func Call2[T1, T2 any](fn func() (T1, T2)) (T1, T2, error)
func Call3[T1, T2, T3 any](fn func() (T1, T2, T3)) (T1, T2, T3, error)
API 按业务返回值数量一一对应。Get3 / Call3 作为多返回值辅助的上限;返回值更多时建议定义 struct,而不是继续扩展 GetN / CallN:
| 业务返回值数量 |
将error 转为 panic |
将 panic 转回error |
| 0 |
OK |
Do |
| 1 |
Get |
Call |
| 2 |
Get2 |
Call2 |
| 3 |
Get3 |
Call3 |
使用 Do
Do 用于没有业务返回值、只需要依次完成多个操作的场景:
err := must.Do(func() {
must.OK(os.MkdirAll(directory, 0o755))
must.OK(os.WriteFile(filename, data, 0o644))
})
if err != nil {
return err
}
如果创建目录失败,不会继续写入文件;如果写入文件失败,Do 会返回 os.WriteFile 产生的原始 error。
使用 Call
Call 用于返回一个业务值:
config, err := must.Call(func() Config {
data := must.Get(os.ReadFile(filename))
var config Config
must.OK(json.Unmarshal(data, &config))
must.OK(config.Validate())
return config
})
读取、解析或校验任意一步失败时,config 为 Config 的零值,err 为失败步骤产生的原始 error。
使用 Call2 和 Call3
Call2 用于返回两个业务值:
user, token, err := must.Call2(func() (User, string) {
user := must.Get(loadUser(userID))
token := must.Get(createToken(user))
must.OK(saveLoginRecord(user, token))
return user, token
})
Call3 用于返回三个业务值:
host, port, secure, err := must.Call3(func() (string, int, bool) {
data := must.Get(os.ReadFile(filename))
config := must.Get(parseServerConfig(data))
must.OK(config.Validate())
return config.Host, config.Port, config.Secure
})
发生错误时,Call2 或 Call3 会将所有业务返回值置为对应类型的零值,并返回原始 error。
错误上下文
must 只负责传播 error,不会自动补充错误上下文。建议仍在错误跨越语义层级的位置使用 %w 包装,例如:
func loadUser(id int64) (User, error) {
user, err := queryUser(id)
if err != nil {
return User{}, fmt.Errorf("query user %d: %w", id, err)
}
return user, nil
}
之后可以继续使用 must.Get(loadUser(id)) 做传播。Do 和 Call 系列返回原始 error,因此不会额外破坏已有的 errors.Is / errors.As 错误链。
panic 处理范围
Do、Call、Call2 和 Call3 只捕获由本包 OK、Get、Get2 或 Get3 触发的 panic。业务代码主动触发的 panic 和 Go runtime panic 不会被吞掉,而是保持原值继续向上传播。
err := must.Do(func() {
value := must.Get(loadValue())
must.OK(saveValue(value))
panic("unexpected")
})
上面的 unexpected 不会被转换为 error,而会继续向上传播。
panic 只能在同一个 goroutine 中被 recover。不要期望外层的 Do 或 Call 捕获子 goroutine 中触发的 panic。
runtime.Goexit 也不会被转换成 error;它会继续终止当前 goroutine。
本库不支持 GODEBUG=panicnil=1 遗留兼容模式。该模式会使 recover 无法区分 panic(nil) 和 runtime.Goexit;请使用 Go 1.25 的默认运行时行为。
不要在可能于 panic 展开阶段执行的 defer 中使用 OK 或 Get 系列函数。例如:
defer func() {
must.OK(file.Close())
}()
如果函数主体已经发生 panic,上面的 defer 再触发一个 must panic,会覆盖原 panic,外层 Do / Call 最终只能看到后一个错误。这是 Go 的 panic/defer 语义导致的。清理逻辑中的错误应使用显式处理。
如果 OK 或 Get 系列触发的 panic 没有被 Do 或 Call 系列捕获,输出会同时包含 must 标记和原始错误消息:
panic: must: 原始错误消息