在 Go 语言还没有泛型支持的年代,开发者面对的一个典型难题是:函数可能返回业务数据和错误,而调用方既需要区分错误的具体类型,又希望在出错时仍能拿到部分已计算的值。只用普通的 error 接口会让类型信息丢失,用 interface{} 又会让值透传变得不安全。下面介绍几种在 Go 无泛型时代被广泛采用的惯用方案。

自定义错误类型承载上下文与业务值
最基础也最有效的做法是定义自己的错误结构体,把错误分类、消息以及需要透传的值都作为字段保存。这样调用方通过类型断言就能拿到结构化信息,而不必解析错误字符串。假设我们有一个读取配置并返回解析后数值的函数,在网络超时的情况下我们仍希望把已读取的字节数传回去。
下面的代码展示了如何定义带有业务值的错误类型。注意 <error> 是标准库接口,我们自己的结构体要实现 Error 方法。通过暴露 TimeoutError 类型,上层可以用 comma-ok 断言判断是不是超时,并读取 BytesRead 字段做断点续传。
package main
import (
"fmt"
)
type TimeoutError struct {
Msg string
BytesRead int
}
func (e *TimeoutError) Error() string {
return fmt.Sprintf("timeout: %s, bytes read: %d", e.Msg, e.BytesRead)
}
func readConfig() (int, error) {
// 模拟读取了一部分数据后超时
return 0, &TimeoutError{Msg: "connection lost", BytesRead: 128}
}
func main() {
val, err := readConfig()
if te, ok := err.(*TimeoutError); ok {
fmt.Println("got timeout, bytes:", te.BytesRead)
} else if err != nil {
fmt.Println("other error:", err)
}
_ = val
}
这种写法的好处是类型安全且自解释,调用方不需要依赖魔法字符串。缺点是每新增一种错误就要定义一个结构,在大型项目里错误类型可能膨胀。不过相比用 map 或全局变量传递上下文,它更符合 Go 显式处理的哲学。
利用闭包与辅助函数做安全的值透传
当函数调用链较深时,如果每一层都只返回 error,业务值就容易在中间被丢弃。一种惯用手法是用闭包把结果变量捕获起来,再借助一个泛型出现前的辅助函数统一做类型安全的断言和提取。这里虽然没有泛型,但可以用 reflect 或者约定好的接口来减少重复代码。
我们可以写一个 UnwrapValue 函数,接收 error 并尝试从中提取实现了特定接口的业务快照。下面的例子用了一个 SnapshotProvider 接口,任何错误只要实现了它就能把中间值交出来。这样在多层调用里,哪怕底层失败了,也能顺着错误拿到快照。
package main
import (
"fmt"
)
type SnapshotProvider interface {
Snapshot() interface{}
}
type StepError struct {
step int
val string
}
func (e *StepError) Error() string {
return fmt.Sprintf("failed at step %d", e.step)
}
func (e *StepError) Snapshot() interface{} {
return e.val
}
func process() (string, error) {
// 第三步出错,但已经算出了部分结果
return "", &StepError{step: 3, val: "partial-data"}
}
func main() {
res, err := process()
if err != nil {
if sp, ok := err.(SnapshotProvider); ok {
fmt.Println("recovered snapshot:", sp.Snapshot())
}
}
_ = res
}
这种方案把透传逻辑收敛到一个接口里,比在每层函数签名里加返回字段要轻量。它要求团队遵守接口约定,但换来了清晰的错误处理路径。在无法使用泛型的旧版本 Go 中,这是兼顾类型安全和代码简洁的可行路线。
对比 panic-recover 与显式错误返回的取舍
有些开发者会想到用 panic 配合 recover 来捕获错误并顺带传递值,因为 recover 能拿到任意 interface{}。但在无泛型时代,这反而更容易破坏类型安全。panic 本来设计用于真正异常的状况,滥用会让控制流难以追踪,而且 recover 拿到的动态类型仍需断言。
显式错误返回配合上文的结构体或接口方案,虽然代码量稍多,但调用方一眼就能看到函数可能失败并携带什么信息。下表简单对比两种方式的差异,帮助你在旧版 Go 项目里做选型。
| 方式 | 类型安全 | 值透传成本 | 可读性 |
|---|---|---|---|
| 自定义错误结构 | 高,编译期可断言 | 低,字段直接带出 | 好,签名明确 |
| panic-recover | 低,运行时才知类型 | 中,需包装在 panic 值里 | 差,隐蔽控制流 |
综合来看,在无泛型时代优先采用自定义错误与接口提取,而不是借助 panic。这样既能保证编译层面的类型检查,也能在出错时把业务值沿着错误链安全交回调用方,符合 Go 社区长期积累的惯用实践。