这个总结的比较好,在此基础上继续总结:
-
使用明确的错误类型和值:定义特定领域的错误类型和值,使错误处理更加明确。
-
在适当的抽象层处理错误:低层函数提供详细错误信息,高层函数作出策略决策。
-
包装错误以保留上下文:使用
fmt.Errorf
和%w
添加上下文而不丢失原始错误。 -
使用
errors.Is
和errors.As
:使用这些函数检查错误类型和值,而不是直接比较。 -
提供结构化错误信息:错误应包含足够的信息以便调试和解决问题。
-
独立的错误处理逻辑:将错误处理逻辑从主要业务逻辑中分离。
-
避免过度使用
panic
:保留panic
用于真正的异常情况,使用error
处理预期的错误。 -
适当聚合多个错误:使用错误聚合处理可能产生多个错误的操作。
-
考虑错误的可重试性:区分临时错误和永久错误,实现智能重试策略。
-
保持错误信息的一致性:在整个代码库中使用一致的错误处理风格和模式。
- 在业务代码中使用wrap error,在基础类库中,我们不应该使用wrap error。基础库中大量自定义的error
建议在error.New时前面用 ‘包名: 错误具体信息’,这样可以知道错误是从哪个包抛出来的,会清晰一点。
原因和理由Claude 3.7 回答如下。
基础类库与业务代码的错误处理策略
错误处理的分层策略
您提到的观点非常有价值:在业务代码中使用 wrapped errors,而基础类库需要采用不同的错误处理方式。这种分层策略有深刻的设计理念和实际考量。
基础类库中的错误处理最佳实践
1. 使用确定性错误值 (Sentinel Errors)
基础类库应该定义和返回明确的、可识别的错误值:
// 定义包级错误变量
var (
ErrInvalidInput = errors.New("invalid input")
ErrNotFound = errors.New("resource not found")
ErrPermission = errors.New("permission denied")
)
// 在函数中返回这些预定义错误
func Parse(input string) (Result, error) {
if !isValid(input) {
return nil, ErrInvalidInput
}
// ...
}
2. 使用错误类型而非字符串拼接
为不同错误情况定义专门的错误类型:
type NotFoundError struct {
Resource string
ID string
}
func (e *NotFoundError) Error() string {
return fmt.Sprintf("%s with id %s not found", e.Resource, e.ID)
}
// 在函数中返回这些错误类型
func FindResource(id string) (*Resource, error) {
// ...
return nil, &NotFoundError{
Resource: "user", ID: id}
}
3. 提供错误检查函数
为错误类型提供公共检查函数,而非依赖类型断言:
// 错误检查函数
func IsNotFound(err error) bool {
_, ok := err.(*NotFoundError)
return ok
}
// 客户端使用
result, err := lib.FindResource(id)
if lib.IsNotFound(err) {
// 处理"未找到"的情况
}
4. 避免在基础类库中过度包装错误
基础类库应保持错误的原始性,避免多层包装:
// 不推荐:在库中过度包装错误
func (s *Store) Get(key string) ([]byte, error) {
data, err := s.db.Get(key)
if err != nil {
return nil, fmt.Errorf("store: failed to get key %q: %w", key, err) // 过度包装
}
return data, nil
}
// 推荐:返回有意义的具体错误
func (s *Store) Get(key string) ([]byte, error) {
data, err := s.db.Get(key)
if err != nil {
if isNotFoundError(err) {
return nil, &KeyNotFoundError{
Key: key} // 转换为领域特定错误
}
return nil, err // 传递底层错误或转换为库的错误类型
}
return data, nil
}
5. 实现支持 errors.Is/As 的错误类型
确保基础库的错误类型实现合适的方法以支持 Go 1.13+ 的错误检查:
type ValidationError struct {
Field string
Err error
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation failed for %s: %v", e.Field, e.Err)
}
// 支持 errors.Unwrap
func (e *ValidationError) Unwrap() error {
return e.Err
}
// 可选:支持 errors.Is 的自定义比较
func (e *ValidationError) Is(target error) bool {
t, ok := target.(*ValidationError)
if !ok {
return false
}
return e.Field == t.Field
}
为什么基础类库和业务代码需要不同的错误处理方式?
1. API 契约与稳定性考量
基础类库需要明确的错误契约:
- 类库是被多个消费者使用的稳定 API
- 错误类型和值构成 API 契约的一部分
- 消费者依赖于稳定的错误类型来编写错误处理逻辑
// 类库代码 - 提供稳定的错误类型
package database
type NotConnectedError struct{
}
func (e NotConnectedError) Error() string {
return "database connection not established"
}
// 消费者代码
err := db.Query(sql)
if _, ok := err.(database.NotConnectedError); ok {
// 重连数据库
db.Reconnect()
}
2. 抽象级别与关注点
基础类库与业务代码处于不同的抽象层次:
- 基础类库关注特定领域错误的精确表达
- 业务代码关注端到端流程和错误上下文链
// 基础类库 - 精确表达错误
func Validate(email string) error {
if !emailRegex.MatchString(email) {
return &ValidationError{
Field: "email", Message: "invalid format"}
}
return nil
}
// 业务代码 - 添加流程上下文
func RegisterUser(user User) error {
if err := validator.Validate(user.Email); err != nil {
return fmt.Errorf("user registration failed: %w", err)
}
// ...
}
3. 复用性与上下文关联
基础类库错误需要通用复用:
- 库的错误应该与特定应用上下文无关
- 业务代码错误通常与特定业务流程紧密关联
// 基础类库 - 通用错误
package jsonparser
var ErrSyntax = errors.New("invalid JSON syntax")
// 业务代码 - 添加特定上下文
func loadUserPreferences(userID string) (*Preferences, error) {
data, err := readUserFile(userID)
if err != nil {
return nil, fmt.Errorf("loading preferences for user %s: %w", userID, err)
}
prefs, err := jsonparser.Parse(data)
if err != nil {
if errors.Is(err, jsonparser.ErrSyntax) {
// 添加业务上下文
return nil, fmt.Errorf("user %s has corrupt preferences file: %w", userID, err)
}
return nil, fmt.Errorf("parsing preferences for user %s: %w", userID, err)
}
return prefs, nil
}
4. 诊断信息与性能权衡
错误包装会增加开销:
- 基础类库通常需要考虑性能和资源使用
- 业务代码更关注可调试性和错误诊断
// 基础类库 - 简单高效的错误
func (cache *Cache) Get(key string) (interface{
}, error) {
if !cache.initialized {
return nil, ErrNotInitialized // 简单错误值,低开销
}
// ...
}
// 业务代码 - 丰富的错误上下文
func fetchUserData(userID string) (*UserData, error) {
data, err := cache.Get(userCacheKey(userID))
if err != nil {
if errors.Is(err, cache.ErrNotInitialized) {
log.Error("Cache subsystem not initialized when fetching user data")
}
return nil, fmt.Errorf("fetching user %s from cache: %w", userID, err) // 添加上下文
}
// ...
}
5. 错误处理职责划分
职责清晰分离:
- 基础类库负责提供准确的错误分类
- 业务代码负责整合错误并添加适当上下文
// 基础类库 - 提供精确的错误类型
package paymentprocessor
type InsufficientFundsError struct {
Account string
Required float64
Available float64
}
func (e *InsufficientFundsError) Error() string {
return fmt.Sprintf("insufficient funds in account %s: required %.2f, available %.2f",
e.Account, e.Required, e.Available)
}
// 业务代码 - 根据错误类型做出适当反应并添加上下文
func processOrder(order Order) error {
err := payments.Charge(order.CustomerID, order.TotalAmount)
if err != nil {
var fundsErr *paymentprocessor.InsufficientFundsError
if errors.As(err, &fundsErr) {
// 特定业务逻辑处理
notifyCustomerAboutFunds(order.CustomerID, fundsErr.Required, fundsErr.Available)
return fmt.Errorf("order %s payment failed due to insufficient funds: %w",
order.ID, err)
}
return fmt.Errorf("order %s payment processing failed: %w", order.ID, err)
}
// ...
}
在实际项目中的应用示例:以 Kubernetes 为例
Kubernetes 作为大型Go项目,很好地展示了这种分层错误处理策略:
基础组件层(apimachinery)使用确定性错误
// kubernetes/staging/src/k8s.io/apimachinery/pkg/api/errors/errors.go
// 定义清晰的错误类型
type StatusError struct {
ErrStatus metav1.Status
}
func (e *StatusError) Error() string {
return e.ErrStatus.Message
}
// 提供错误检查函数
func IsNotFound(err error) bool {
return reasonForError(err) == metav1.StatusReasonNotFound
}
func IsAlreadyExists(err error) bool {
return reasonForError(err) == metav1.StatusReasonAlreadyExists
}
控制器层(业务逻辑)使用错误包装
// kubernetes/pkg/controller/deployment/deployment_controller.go
func (dc *DeploymentController) syncDeployment(ctx context.Context, key string) error {
// ...
deployment, err := dc.dLister.Deployments(namespace).Get(name)
if err != nil {
if apierrors.IsNotFound(err) {
// 特定错误类型的处理
dc.queue.Forget(key)
return nil
}
return fmt.Errorf("failed to get deployment %s/%s: %w", namespace, name, err)
}
// ...
if err := dc.scaleDeployment(deployment, newRS); err != nil {
// 包装错误并添加业务上下文
return fmt.Errorf("failed to scale deployment %s/%s: %w",
deployment.Namespace, deployment.Name, err)
}
// ...
}
实践建议:基础类库错误处理的关键原则
-
定义清晰的错误类型和值
- 创建表达特定错误条件的类型
- 避免使用通用字符串错误
-
提供错误检查函数
- 不要期望调用者使用类型断言
- 为每种重要错误条件提供
IsXxx(error) bool
函数
-
保持错误信息简洁但完整
- 包含必要信息,但不包含应用级别上下文
- 确保错误消息对库的使用者有意义
-
实现标准接口支持
- 实现
Unwrap()
方法支持错误链 - 需要时提供自定义
Is()
和As()
方法
- 实现
-
考虑错误的国际化和安全性
- 避免在错误消息中包含敏感信息
- 考虑错误消息的可翻译性
-
文档化错误行为
- 在公共API文档中清晰说明可能的错误条件
- 告知使用者如何正确检查和处理错误
总结
在软件架构中采用分层的错误处理策略是一种成熟的做法。基础类库应该提供明确的错误类型和值,专注于准确表达领域错误,而不添加应用特定上下文。业务代码则应负责包装这些错误,添加流程上下文,并决定如何对不同错误情况作出反应。
这种分层策略遵循"关注点分离"原则,使系统的不同部分能够专注于自身的核心责任,从而创建更加健壮、可维护和可演化的软件。