一、Go 错误处理哲学:显式优于隐式
Go 语言的错误处理机制自诞生以来就是最具争议的特性之一。相比 Java 的异常抛出机制(try-catch)或 Rust 的 Result 类型,Go 选择了一条更加直接和显式的道路:每个可能出错的地方都返回 error 接口,调用者必须立即处理或传播。
这种设计的哲学基础是显式优于隐式(Explicit is better than implicit)。在 Java 中,异常可以跨越数十层调用栈向上抛出,直到某个遥远的 catch 块捕获它。这固然简化了中间层的代码,但也带来了灾难性的后果:没有人知道一个方法会抛出哪些异常,异常的真正来源可能被深埋在堆栈的最底层。相比之下,Go 的 if err != nil 虽然显得啰嗦,但让每一层调用都显式参与到错误处理中来——错误不会悄悄溜走。
Go 的错误处理遵循一个核心原则:错误是值。error 只是一个接口,任何实现了 Error() string 方法的类型都可以作为错误值。这意味着错误可以携带任意上下文信息——错误码、堆栈、请求ID、时间戳、重试次数等。将错误作为值处理,而不是作为特殊的控制流机制,这是 Go 区别于大多数语言的根本特征。
Go 1.13 在语言层面增强了错误处理的能力,通过 %w 动词支持错误包装,并提供了 errors.Is 和 errors.As 两个核心函数。但这些工具仍然是基础性的——将它们组合成一套完整的企业级错误处理体系,需要系统性的设计。
二、errors 包源码解析:从 New 到 Wrap
标准的 errors 包代码量极小,但每一行都经过精心设计。首先看最基础的 errors.New:
func New(text string) error {
return &errorString{text}
}
type errorString struct {
s string
}
func (e *errorString) Error() string {
return e.s
}
New 返回一个指向 errorString 结构体的指针。为什么用指针而不是值?因为如果是值类型,两个内容相同的错误会互相相等。通过返回指针,每个 errors.New("xxx") 创建的错误都是唯一的实例,可以被 == 比较。这就是 errors.New 创建的 sentinel error(哨兵错误)可以被 errors.Is 识别的基础。
fmt.Errorf 在 Go 1.13 之前只是格式化字符串并调用 errors.New:
// Go 1.13 之前
err := fmt.Errorf("查询用户失败: %v", err)
但从 Go 1.13 起,fmt.Errorf 支持 %w 动词,生成一个包装错误:
err := fmt.Errorf("查询用户失败: %w", err)
这里的 %w 不是简单地将错误嵌入字符串,而是在内部创建一个 wrapError 结构体:
type wrapError struct {
msg string
err error
}
func (e *wrapError) Error() string {
return e.msg
}
func (e *wrapError) Unwrap() error {
return e.err
}
关键的 Unwrap() error 方法是 Go 1.13 错误链机制的核心。通过不断调用 Unwrap,可以从外向内逐层解开错误包装,直到找到根因错误。errors.Is 的实现正是利用了这一点:
func Is(err, target error) bool {
if target == nil {
return err == target
}
isComparable := reflectlite.TypeOf(target).Comparable()
for {
if isComparable && err == target {
return true
}
if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) {
return true
}
switch x := err.(type) {
case interface{ Unwrap() error }:
err = x.Unwrap()
if err == nil {
return false
}
default:
return false
}
}
}
errors.Is 的处理逻辑非常清晰:
- 如果
target是 nil,直接比较。 - 使用
==判断当前错误是否就是目标错误。这要求错误类型是可比较的(comparable)。 - 如果当前错误实现了自定义的
Is(error) bool方法,调用它进行判断。 - 如果当前错误实现了
Unwrap() error,解包一层继续比较。 - 如果无法解包且不匹配,返回 false。
errors.As 则用于从错误链中提取特定类型的错误:
func As(err error, target interface{}) bool {
if target == nil {
panic("errors: target cannot be nil")
}
val := reflectlite.ValueOf(target)
typ := val.Type()
if typ.Kind() != reflectlite.Ptr || val.IsNil() {
panic("errors: target must be a non-nil pointer")
}
targetType := typ.Elem()
if targetType.Kind() != reflectlite.Interface && !targetType.Implements(errorType) {
panic("errors: *target must be interface or implement error")
}
for {
if reflectlite.TypeOf(err).AssignableTo(targetType) {
val.Elem().Set(reflectlite.ValueOf(err))
return true
}
if x, ok := err.(interface{ As(interface{}) bool }); ok && x.As(target) {
return true
}
switch x := err.(type) {
case interface{ Unwrap() error }:
err = x.Unwrap()
if err == nil {
return false
}
default:
return false
}
}
}
errors.As 的用法通常是这样的:
var netErr *net.OpError
if errors.As(err, &netErr) {
fmt.Println("网络错误:", netErr.Op)
}
它会从错误链由内向外逐层匹配,如果找到目标类型的错误,就通过反射赋值给传入的指针。这比 Go 1.13 之前使用类型断言遍历错误链要优雅得多。
三、第三方错误库对比:pkg/errors 与继任者
github.com/pkg/errors
pkg/errors 是 Go 生态中最有影响力的第三方错误库,由 Dave Cheney 创建。它在 Go 1.13 之前提供了 Wrap、WithStack、WithMessage 等功能:
import "github.com/pkg/errors"
// 包装错误并附加堆栈
err = errors.Wrap(err, "数据库查询失败")
// 只附加消息,不包装
err = errors.WithMessage(err, "附加消息")
// 获取根因
cause := errors.Cause(err)
pkg/errors 的核心贡献是将堆栈追踪引入 Go 错误处理。它的 Wrap 不是简单地拼接字符串,而是记录了调用栈的快照:
type withStack struct {
error
*stack
}
当错误最终打印时,%+v 格式化会输出完整的堆栈信息:
数据库查询失败
main.processUser
/project/main.go:42
main.main
/project/main.go:28
--- 根因: sql: no rows in result set
但 pkg/errors 有局限:它维护于 Go 1.13 之前,与标准库的 %w 和 Unwrap 机制不完全兼容。它不再主动维护(已被归档),新项目不应直接使用。
github.com/cockroachdb/errors
这是目前最推荐的第三方错误库,由 CockroachDB 团队维护。它兼容 Go 1.13+ 的标准库机制,同时提供了丰富的扩展:
import "github.com/cockroachdb/errors"
// 包装并保留堆栈
err = errors.Wrap(err, "处理失败")
// 错误码
err = errors.WithTelemetry(err, "查询超时")
// 安全详情(脱敏信息)
err = errors.WithSafeDetails(err, "user_id", 12345)
// 完整格式化
fmt.Printf("%+v\n", err) // 输出堆栈、链、提示等
cockroachdb/errors 的一个创新点是错误提示链(HINT/DETAIL)。它可以在错误上附加结构化信息,而不会影响错误消息本身:
err = errors.WithHint(err, "请检查数据库连接配置")
err = errors.WithDetail(err, "连接超时发生在从服务器 10.0.0.5 读取数据时")
这些信息在日志中会以结构化方式输出,方便监控系统解析。
go.uber.org/multierr
在处理需要聚合多个错误的情况时(如并行验证多个字段),multierr 提供了优雅的解决方案:
import "go.uber.org/multierr"
func validate(input UserInput) error {
var errs error
if input.Name == "" {
errs = multierr.Append(errs, errors.New("姓名不能为空"))
}
if input.Age < 0 {
errs = multierr.Append(errs, errors.New("年龄不能为负数"))
}
if input.Email == "" {
errs = multierr.Append(errs, errors.New("邮箱不能为空"))
}
return errs
}
// 使用
if err := validate(input); err != nil {
for _, e := range multierr.Errors(err) {
log.Println("验证错误:", e)
}
}
multierr 返回的错误实现了错误链,可以与 errors.Is 和 errors.As 协同工作:
if errors.Is(err, ErrNameEmpty) {
// 可以匹配到聚合错误中的特定错误
}
四、企业级错误码设计规范
在企业级系统中,错误码是服务间通信的语言。一个好的错误码设计需要考虑 HTTP 状态码与业务错误码的映射、错误码的分层和范围、以及可扩展性。
HTTP 状态码 vs 业务错误码
HTTP 状态码用于传输层语义,业务错误码用于应用层语义,两者是互补关系:
| HTTP 状态码 | 语义 | 典型业务场景 |
|---|---|---|
| 200 | 成功 | 请求处理成功 |
| 400 | 请求参数错误 | 参数缺失、格式错误、验证失败 |
| 401 | 未认证 | Token 过期、未登录 |
| 403 | 无权限 | 角色不足、资源越权 |
| 404 | 资源不存在 | 用户不存在、订单不存在 |
| 409 | 资源冲突 | 重复提交、并发修改冲突 |
| 422 | 语义错误 | 业务规则校验失败 |
| 429 | 请求过多 | 限流触发 |
| 500 | 服务端错误 | 数据库连接失败、空指针 |
| 502/503/504 | 网关/超时错误 | 下游服务不可用 |
业务错误码采用分层数字编码,建议如下格式:
[系统][模块][级别][序号]
1 2 3 4
例如 1001001 表示:
1- 用户服务系统001- 认证模块0- 信息级别(0=信息/1=警告/2=错误/3=致命)01- 具体错误序号
完整的企业级错误码定义文件:
package errcode
// 公共错误码(000 开头)
const (
Success = 0
ErrInternal = 1000000 // 内部错误
ErrParamInvalid = 1000001 // 参数非法
ErrUnauthorized = 1000002 // 未认证
ErrForbidden = 1000003 // 无权限
ErrNotFound = 1000004 // 资源不存在
ErrTooManyRequest = 1000005 // 请求过于频繁
)
// 用户服务错误码(101 开头)
const (
ErrUserNotFound = 1010001 // 用户不存在
ErrUserExist = 1010002 // 用户已存在
ErrPasswordWrong = 1010003 // 密码错误
ErrTokenExpired = 1010004 // Token 已过期
ErrTokenInvalid = 1010005 // Token 无效
)
// 订单服务错误码(102 开头)
const (
ErrOrderNotFound = 1020001 // 订单不存在
ErrOrderPaid = 1020002 // 订单已支付
ErrOrderCancelled = 1020003 // 订单已取消
ErrInventoryShort = 1020004 // 库存不足
ErrPriceChanged = 1020005 // 价格已变更
)
错误码必须与消息、HTTP 状态码建立映射关系:
package errcode
import "net/http"
type codedError struct {
code int
msg string
httpStatus int
}
func (e *codedError) Error() string { return e.msg }
func (e *codedError) Code() int { return e.code }
func (e *codedError) StatusCode() int { return e.httpStatus }
var codeMap = map[int]*codedError{
ErrInternal: {ErrInternal, "服务器内部错误", http.StatusInternalServerError},
ErrParamInvalid: {ErrParamInvalid, "请求参数非法", http.StatusBadRequest},
ErrUnauthorized: {ErrUnauthorized, "请先登录", http.StatusUnauthorized},
ErrForbidden: {ErrForbidden, "权限不足", http.StatusForbidden},
ErrNotFound: {ErrNotFound, "资源不存在", http.StatusNotFound},
ErrUserNotFound: {ErrUserNotFound, "用户不存在", http.StatusNotFound},
ErrOrderNotFound: {ErrOrderNotFound, "订单不存在", http.StatusNotFound},
ErrInventoryShort: {ErrInventoryShort, "库存不足", http.StatusBadRequest},
// ... 更多映射
}
func New(code int) error {
if ce, ok := codeMap[code]; ok {
return &codedError{code: ce.code, msg: ce.msg, httpStatus: ce.httpStatus}
}
return &codedError{code: ErrInternal, msg: "未知错误", httpStatus: http.StatusInternalServerError}
}
func NewWithMessage(code int, msg string) error {
if ce, ok := codeMap[code]; ok {
return &codedError{code: ce.code, msg: msg, httpStatus: ce.httpStatus}
}
return &codedError{code: ErrInternal, msg: msg, httpStatus: http.StatusInternalServerError}
}
func HTTPStatus(err error) int {
if ce, ok := err.(*codedError); ok {
return ce.httpStatus
}
if errors.Is(err, context.DeadlineExceeded) {
return http.StatusGatewayTimeout
}
return http.StatusInternalServerError
}
五、错误包装与堆栈追踪最佳实践
在企业级代码中,错误经常需要在多个服务之间传递。传递过程中需要保留原始错误信息,同时不断添加上下文。这就是错误包装(Error Wrapping)要做的事情。
层与层之间的错误包装约定
在微服务架构中,建议采用如下错误传播策略:
[数据层] sql.ErrNoRows →
[仓库层] fmt.Errorf("查询用户 %d: %w", id, err) →
[服务层] fmt.Errorf("获取用户信息失败: %w", err) →
[API 层] 返回 JSON 错误响应
每一层只添加本层的上下文信息,不使用 %v(避免破坏错误链),始终使用 %w:
// 数据层
if err := db.QueryRowContext(ctx, "SELECT * FROM users WHERE id = ?", id).Scan(&user); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, fmt.Errorf("用户 %d 不存在: %w", id, errcode.New(errcode.ErrUserNotFound))
}
return nil, fmt.Errorf("数据库查询用户 %d 失败: %w", id, err)
}
// 服务层
func GetUser(ctx context.Context, id int64) (*User, error) {
user, err := repo.FindByID(ctx, id)
if err != nil {
return nil, fmt.Errorf("获取用户失败: %w", err)
}
return user, nil
}
// 处理层
func HandleGetUser(w http.ResponseWriter, r *http.Request) {
id, _ := strconv.ParseInt(r.URL.Query().Get("id"), 10, 64)
user, err := service.GetUser(r.Context(), id)
if err != nil {
// 统一处理:记录日志、返回标准化响应
respondError(w, err)
return
}
respondJSON(w, user)
}
使用第三方库增强堆栈信息
在生产环境中,仅靠 Go 标准库无法获取错误发生时的调用堆栈。使用 github.com/cockroachdb/errors:
import "github.com/cockroachdb/errors"
func criticalOperation() error {
if err := doSomething(); err != nil {
return errors.Wrap(err, "关键操作失败")
}
return nil
}
// 打印时输出完整堆栈
fmt.Printf("%+v\n", err)
输出示例:
关键操作失败:
(1) attached stack trace
-- stack trace:
| main.criticalOperation
| /project/main.go:23
| main.main
| /project/main.go:15
Wraps: (2) 底层错误信息
如果因为某些原因不能使用第三方库,可以在项目内部实现一个简化版:
package errors
import (
"fmt"
"runtime"
"strings"
)
type stacktraceError struct {
msg string
stack []uintptr
cause error
}
func (e *stacktraceError) Error() string { return e.msg }
func (e *stacktraceError) Unwrap() error { return e.cause }
func Wrap(err error, msg string) error {
if err == nil {
return nil
}
const depth = 32
var pcs [depth]uintptr
n := runtime.Callers(2, pcs[:])
return &stacktraceError{
msg: msg,
stack: pcs[:n],
cause: err,
}
}
func FormatStack(e error) string {
if se, ok := e.(*stacktraceError); ok {
var sb strings.Builder
sb.WriteString(se.msg)
sb.WriteString("\nStack trace:\n")
frames := runtime.CallersFrames(se.stack)
for {
frame, more := frames.Next()
sb.WriteString(fmt.Sprintf(" %s\n %s:%d\n", frame.Function, frame.File, frame.Line))
if !more {
break
}
}
if se.cause != nil {
sb.WriteString("Caused by: ")
sb.WriteString(FormatStack(se.cause))
}
return sb.String()
}
return e.Error()
}
六、统一错误拦截:HTTP Middleware 与 gRPC Interceptor
在企业级服务中,需要在最外层统一处理错误,将内部错误翻译成标准的 API 响应。这通过 HTTP Middleware 或 gRPC Interceptor 实现。
HTTP 统一错误处理中间件
package middleware
import (
"encoding/json"
"errors"
"net/http"
"github.com/cockroachdb/errors"
)
type ErrorResponse struct {
Code int `json:"code"`
Message string `json:"message"`
Details []string `json:"details,omitempty"`
}
func ErrorHandler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 使用自定义 ResponseWriter 捕获 panic
defer func() {
if rec := recover(); rec != nil {
// 记录 panic 堆栈
stack := errors.Wrap(errors.Newf("panic: %v", rec), "panic recovered")
logger.Error(r.Context(), "%+v", stack)
respondError(w, http.StatusInternalServerError, ErrorResponse{
Code: 1000000,
Message: "服务器内部错误",
})
}
}()
w.Header().Set("Content-Type", "application/json")
next.ServeHTTP(&responseRecorder{ResponseWriter: w, statusCode: 200}, r)
})
}
type responseRecorder struct {
http.ResponseWriter
statusCode int
wrote bool
}
func (rr *responseRecorder) WriteHeader(code int) {
if !rr.wrote {
rr.statusCode = code
rr.ResponseWriter.WriteHeader(code)
rr.wrote = true
}
}
func respondError(w http.ResponseWriter, statusCode int, resp ErrorResponse) {
w.WriteHeader(statusCode)
json.NewEncoder(w).Encode(resp)
}
将业务错误转为 HTTP 响应
func RespondFromError(w http.ResponseWriter, err error) {
if err == nil {
return
}
// 尝试从错误中提取业务错误码
var ce *errcode.codedError
if errors.As(err, &ce) {
respondError(w, ce.StatusCode(), ErrorResponse{
Code: ce.Code(),
Message: ce.Error(),
})
return
}
// 上下文超时
if errors.Is(err, context.DeadlineExceeded) {
respondError(w, http.StatusGatewayTimeout, ErrorResponse{
Code: 1000006,
Message: "请求处理超时",
})
return
}
// 其他内部错误
respondError(w, http.StatusInternalServerError, ErrorResponse{
Code: 1000000,
Message: "服务器内部错误",
})
}
gRPC 统一错误拦截器
func UnaryErrorInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
resp, err := handler(ctx, req)
if err == nil {
return resp, nil
}
// 已经是 gRPC 状态错误,直接返回
if _, ok := status.FromError(err); ok {
return nil, err
}
// 业务错误转 gRPC 状态码
var ce *errcode.codedError
if errors.As(err, &ce) {
st := status.New(grpcCodeFromHTTP(ce.StatusCode()), ce.Error())
ds, _ := st.WithDetails(&errpb.ErrorDetail{
Code: int32(ce.Code()),
Message: ce.Error(),
})
return nil, ds.Err()
}
if errors.Is(err, context.DeadlineExceeded) {
return nil, status.Error(codes.DeadlineExceeded, "请求超时")
}
return nil, status.Error(codes.Internal, "内部错误")
}
func grpcCodeFromHTTP(httpCode int) codes.Code {
switch httpCode {
case http.StatusBadRequest:
return codes.InvalidArgument
case http.StatusUnauthorized:
return codes.Unauthenticated
case http.StatusForbidden:
return codes.PermissionDenied
case http.StatusNotFound:
return codes.NotFound
case http.StatusTooManyRequests:
return codes.ResourceExhausted
default:
return codes.Internal
}
}
七、错误与日志、监控、链路追踪的集成
在现代云原生架构中,错误处理不仅是给用户的反馈,更是运维可观测性的核心数据来源。错误必须与分布式追踪、指标监控、日志系统深度集成。
日志中的错误记录
package logger
import (
"context"
"fmt"
"github.com/cockroachdb/errors"
"go.uber.org/zap"
)
func Error(ctx context.Context, format string, args ...interface{}) {
// 从 context 中提取 traceID
traceID, _ := ctx.Value(TraceKey{}).(string)
// 最后一个参数如果是 error,特殊处理
var err error
if len(args) > 0 {
if e, ok := args[len(args)-1].(error); ok {
err = e
args = args[:len(args)-1]
}
}
fields := []zap.Field{
zap.String("trace_id", traceID),
zap.String("message", fmt.Sprintf(format, args...)),
}
if err != nil {
fields = append(fields,
zap.String("error", err.Error()),
zap.String("error_verbose", fmt.Sprintf("%+v", err)),
)
// 尝试提取错误码
if ce, ok := err.(interface{ Code() int }); ok {
fields = append(fields, zap.Int("error_code", ce.Code()))
}
}
zap.L().Error("error occurred", fields...)
}
使用方式:
if err := processOrder(ctx, req); err != nil {
logger.Error(ctx, "处理订单 %d 失败", req.OrderID, err)
return err
}
错误指标上报
使用 Prometheus 统计错误:
var (
errorCounter = prometheus.NewCounterVec(prometheus.CounterOpts{
Name: "app_errors_total",
Help: "应用错误总数",
}, []string{"code", "module"})
errorLatency = prometheus.NewHistogramVec(prometheus.HistogramOpts{
Name: "app_error_latency_seconds",
Help: "错误发生时的延迟",
Buckets: prometheus.DefBuckets,
}, []string{"code"})
)
func RecordError(err error, module string, latency time.Duration) {
code := "unknown"
if ce, ok := err.(interface{ Code() int }); ok {
code = fmt.Sprintf("%d", ce.Code())
} else if errors.Is(err, context.DeadlineExceeded) {
code = "timeout"
}
errorCounter.WithLabelValues(code, module).Inc()
errorLatency.WithLabelValues(code).Observe(latency.Seconds())
}
链路追踪中的错误标记
import "go.opentelemetry.io/otel/trace"
func tracedOperation(ctx context.Context) error {
ctx, span := tracer.Start(ctx, "processOrder")
defer span.End()
if err := validateOrder(ctx); err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
}
if err := chargeOrder(ctx); err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "扣款失败")
return fmt.Errorf("扣款失败: %w", err)
}
span.SetStatus(codes.Ok, "")
return nil
}
通过 span.RecordError 和 span.SetStatus,错误信息会自动同步到分布式追踪系统(如 Jaeger、Zipkin)中,开发者可以在链路图上直观地看到哪一步发生了错误。
八、错误处理的性能考量
在企业级系统中,错误路径虽然不常发生,但一旦出错可能面临大量错误同时涌现的场景(如数据库连接池耗尽后的雪崩)。错误处理的性能不容忽视。
避免在热路径中频繁分配错误对象
// 不好:每次调用都创建新错误
func Check(v int) error {
if v < 0 {
return errors.New("值不能为负数") // 每次 GC 都会处理
}
return nil
}
// 好:使用预定义的哨兵错误
var ErrNegativeValue = errors.New("值不能为负数")
func Check(v int) error {
if v < 0 {
return ErrNegativeValue // 零分配
}
return nil
}
延迟格式化字符串
// 不好:即使 err 为 nil,也会格式化字符串
func DoSomething(id int) error {
fmtErr := fmt.Sprintf("处理 ID %d 失败", id)
if err := internalWork(); err != nil {
return errors.New(fmtErr)
}
return nil
}
// 好:仅在错误发生时格式化
func DoSomething(id int) error {
if err := internalWork(); err != nil {
return fmt.Errorf("处理 ID %d 失败: %w", id, err)
}
return nil
}
堆栈追踪的性能影响
获取堆栈信息是相对昂贵的操作。在极高并发的服务中,频繁地生成堆栈追踪会成为瓶颈。
// 在关键路径中使用简化的错误包装
func hotPath() error {
if err := db.Query(); err != nil {
// 用轻量方式标记,不取堆栈
return fmt.Errorf("DB失败: %w", err)
}
return nil
}
// 在 HTTP 处理层统一附加堆栈
func handler(w http.ResponseWriter, r *http.Request) {
if err := hotPath(); err != nil {
// 在这里取堆栈,因为频率已经降低了
log.Printf("%+v", errors.WithStack(err))
}
}
另一个技巧是使用 runtime.Caller 而非 runtime.Callers 来只获取最近的调用者,而非完整堆栈。
错误类型的内存布局
在频繁创建短生命周期错误对象的场景中,使用值类型而非指针类型可以减少 GC 压力:
// 值类型错误(零分配,适合高频场景)
type CodeError struct {
Code int
Msg string
}
func (e CodeError) Error() string { return e.Msg }
// 使用
return CodeError{Code: 1001, Msg: "参数错误"}
但值类型错误无法在 errors.As 中正确匹配(因为它需要可寻址的值),所以只应在不需要类型断言的场景中使用。
九、企业级错误处理框架完整案例
以下是一个接近生产级的完整错误处理框架设计,集成了错误码、堆栈追踪、日志、Metrics 和标准化响应:
package apperror
import (
"encoding/json"
"fmt"
"net/http"
"runtime"
"time"
"github.com/prometheus/client_golang/prometheus"
)
// ErrorCode 错误码类型
type ErrorCode int
// 核心错误码定义
const (
CodeOK ErrorCode = 0
CodeInternal ErrorCode = 1000000
CodeParamInvalid ErrorCode = 1000001
CodeUnauthorized ErrorCode = 1000002
CodeForbidden ErrorCode = 1000003
CodeNotFound ErrorCode = 1000004
CodeTimeout ErrorCode = 1000006
CodeTooManyRequests ErrorCode = 1000007
)
// AppError 应用错误结构
type AppError struct {
Code ErrorCode
Message string
cause error
stack []uintptr
timestamp time.Time
details map[string]interface{}
}
// 确保 AppError 实现 error 接口
func (e *AppError) Error() string {
if e.cause != nil {
return fmt.Sprintf("[%d] %s: %v", e.Code, e.Message, e.cause)
}
return fmt.Sprintf("[%d] %s", e.Code, e.Message)
}
// Unwrap 支持 errors.Is 和 errors.As
func (e *AppError) Unwrap() error {
return e.cause
}
// WithDetail 附加结构化详情(链式调用)
func (e *AppError) WithDetail(key string, value interface{}) *AppError {
if e.details == nil {
e.details = make(map[string]interface{})
}
e.details[key] = value
return e
}
// HTTPStatus 映射到 HTTP 状态码
func (e *AppError) HTTPStatus() int {
switch {
case e.Code >= 1000000 && e.Code < 1000100:
return http.StatusBadRequest
case e.Code == CodeInternal:
return http.StatusInternalServerError
case e.Code == CodeUnauthorized:
return http.StatusUnauthorized
case e.Code == CodeForbidden:
return http.StatusForbidden
case e.Code == CodeNotFound:
return http.StatusNotFound
case e.Code == CodeTimeout:
return http.StatusGatewayTimeout
case e.Code == CodeTooManyRequests:
return http.StatusTooManyRequests
default:
return http.StatusInternalServerError
}
}
// FormatStack 格式化堆栈
func (e *AppError) FormatStack() string {
if len(e.stack) == 0 {
return ""
}
var sb strings.Builder
frames := runtime.CallersFrames(e.stack)
for {
frame, more := frames.Next()
sb.WriteString(fmt.Sprintf("%s\n %s:%d\n", frame.Function, frame.File, frame.Line))
if !more {
break
}
}
return sb.String()
}
// 错误构造函数
func New(code ErrorCode, msg string) *AppError {
return &AppError{
Code: code,
Message: msg,
timestamp: time.Now(),
}
}
func Wrap(cause error, code ErrorCode, msg string) *AppError {
const depth = 32
var pcs [depth]uintptr
n := runtime.Callers(2, pcs[:])
return &AppError{
Code: code,
Message: msg,
cause: cause,
stack: pcs[:n],
timestamp: time.Now(),
}
}
// Wrapf 格式化包装
func Wrapf(cause error, code ErrorCode, format string, args ...interface{}) *AppError {
return Wrap(cause, code, fmt.Sprintf(format, args...))
}
// 预定义的错误响应
func ErrInternal(msg string) *AppError {
return New(CodeInternal, msg)
}
func ErrParam(msg string) *AppError {
return New(CodeParamInvalid, msg)
}
func ErrNotFound(resource string) *AppError {
return New(CodeNotFound, fmt.Sprintf("%s 不存在", resource))
}
// HTTP 响应辅助
func RespondJSON(w http.ResponseWriter, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(data)
}
type errorJSON struct {
Code int `json:"code"`
Message string `json:"message"`
Details map[string]interface{} `json:"details,omitempty"`
Timestamp string `json:"timestamp"`
}
func RespondError(w http.ResponseWriter, err error) {
var appErr *AppError
if !errors.As(err, &appErr) {
appErr = Wrap(err, CodeInternal, "未分类错误")
}
resp := errorJSON{
Code: int(appErr.Code),
Message: appErr.Message,
Details: appErr.details,
Timestamp: appErr.timestamp.Format(time.RFC3339),
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(appErr.HTTPStatus())
json.NewEncoder(w).Encode(resp)
}
// Metrics 集成
var errorCounter = prometheus.NewCounterVec(prometheus.CounterOpts{
Name: "app_error_total",
Help: "Total number of application errors",
}, []string{"code"})
func init() {
prometheus.MustRegister(errorCounter)
}
func (e *AppError) Record() {
errorCounter.WithLabelValues(fmt.Sprintf("%d", e.Code)).Inc()
}
框架使用示例
package main
import (
"database/sql"
"encoding/json"
"errors"
"fmt"
"net/http"
"myproject/apperror"
_ "github.com/mattn/go-sqlite3"
)
var db *sql.DB
func init() {
var err error
db, err = sql.Open("sqlite3", "test.db")
if err != nil {
panic(err)
}
}
// 领域层
func getUserFromDB(id int64) (*User, error) {
var u User
err := db.QueryRow("SELECT id, name, email FROM users WHERE id = ?", id).Scan(&u.ID, &u.Name, &u.Email)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, apperror.New(apperror.CodeNotFound, "用户不存在")
}
return nil, apperror.Wrap(err, apperror.CodeInternal, "数据库查询失败")
}
return &u, nil
}
// 应用层
func getUserService(id int64) (*User, error) {
user, err := getUserFromDB(id)
if err != nil {
return nil, apperror.Wrapf(err, apperror.CodeInternal, "获取用户 %d 失败", id)
}
return user, nil
}
// 接口层
func handleGetUser(w http.ResponseWriter, r *http.Request) {
// 解析参数
var req struct {
ID int64 `json:"id"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
ae := apperror.ErrParam("请求参数格式错误")
ae.Record()
apperror.RespondError(w, ae)
return
}
// 业务处理
user, err := getUserService(req.ID)
if err != nil {
var appErr *apperror.AppError
if errors.As(err, &appErr) {
appErr.Record()
}
apperror.RespondError(w, err)
return
}
// 成功响应
apperror.RespondJSON(w, user)
}
type User struct {
ID int64 `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
func main() {
http.HandleFunc("/user", handleGetUser)
fmt.Println("Server on :8080")
http.ListenAndServe(":8080", nil)
}
这个框架的设计要点:
- 错误码强类型化:
ErrorCode是 int 别名而非原生 int,增强类型安全。 - 链式 API:
WithDetail支持链式调用,方便构造错误。 - 自动堆栈采集:
Wrap中自动取堆栈,不需要手动调用。 - 标准化 HTTP 响应:通过
RespondError统一输出格式。 - Prometheus 集成:错误自动上报 metrics。
- 零依赖选项:框架本身只依赖标准库 + Prometheus,第三方堆栈库是可选增强。
十、总结
Go 的错误处理从表面上看似乎简陋——没有 try-catch、泛型的缺失让错误处理更加啰嗦。但深入剖析后会发现,这套机制经过精心设计,if err != nil 的背后是显式控制流、错误即值、接口组合等坚实的工程原则。
企业级错误处理体系的核心在于以下几个方面:
错误包装链是调试的生命线。通过 %w 和 Unwrap,Go 1.13 实现了错误链的标准化。每一层函数都应该添加本层的上下文信息,让最终看到的错误是一条完整的故事线:从 HTTP 请求到数据库查询,每一站的上下文都清晰可查。
业务错误码是服务间通信的通用语言。错误码的设计应该分层、分范围,并与 HTTP 状态码建立明确的映射关系。一张团队公认的错误码表是协作的基础。
统一拦截层是 API 质量的守门员。通过 HTTP Middleware 和 gRPC Interceptor,在系统的边界处统一处理错误转换,让内部代码只关注业务逻辑,不关心 HTTP 状态码或 gRPC 的 codes.Code。
可观测性是错误处理的高级形态。错误必须与日志、metrics、链路追踪系统集成。一个错误如果没有被记录在正确的 trace 上下文中,就如同大海捞针。span.RecordError 和结构化日志是现代服务不可或缺的工具。
性能是不能忽略的因素。堆栈追踪虽然强大但昂贵,错误对象的频繁分配在高并发场景下会成为瓶颈。只在必要的时候取堆栈,重用哨兵错误,延迟格式化字符串,这些都是生产环境的必修功课。
从 errors.New 到 fmt.Errorf("%w"),从 pkg/errors 到 cockroachdb/errors,从原始的错误字符串到完整的可观测错误体系,Go 社区在错误处理领域已经积累了近十年的实践智慧。这些经验不是对 if err != nil 的厌倦,而是对它更深层次、更工程化的运用。掌握了这套体系,就能在 Go 的企业级开发中游刃有余。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。