乐趣区

关于golang:最佳实践之Golang错误处理

1、原生错误处理

Go 语言通过内置的谬误接口提供了非常简单的错误处理机制。
error 类型是一个接口类型,这是它的定义:
type error interface {
    Error() string
}

咱们能够在编码中通过实现 error 接口类型来生成错误信息。
函数通常在最初的返回值中返回错误信息。应用 errors.New 可返回一个错误信息:

func Sqrt(f float64) (float64, error) {
    if f < 0 {return 0, errors.New("math: square root of negative number")
    }
    // 实现
}

在上面的例子中,咱们在调用 Sqrt 的时候传递的一个正数,而后就失去了 non-nil 的 error 对象,将此对象与 nil 比拟,后果为 true,所以 fmt.Println(fmt 包在解决 error 时会调用 Error 办法)被调用,以输入谬误,请看上面调用的示例代码:

result, err:= Sqrt(-1)
if err != nil {fmt.Println(err)
}

2、开源 error 包

github.com/pkg/errors 包在原生 error 包根底上减少了以下罕用的性能:

  • 能够打印 error 的堆栈信息:打印谬误须要 %+ v 能力具体输入
  • 应用 Wrap 或 Wrapf,初始化一个 error
  • 应用 errors.WithMessage 能够在原来的 error 根底上再包装一层,蕴含原有 error 信息
  • errors.Is,用于判断 error 类型,可依据 error 类型不同做不同解决
  • errors.As,用于解析 error

具体应用案例见全局错误处理一节。

3、工程中错误处理

3.1 需要整顿

  • 自定义 error 信息,并进行编码整顿

    • controller 层能够判断自定义 error 类型,最终判断是按 info 解决,还是按 error 解决
  • 能够打印 error 初始产生的地位(获取 error 的调用栈)
  • 确认以后零碎定位:

    • 用户,获取 TagMessage
    • 上游服务,须要错误码映射
    • 日志监控、监控 TagMessage

上面在一个工程化的我的项目中利用 github.com/pkg/errors 包,残缺实现一套的错误处理机制

3.2 形式一:Map 保留错误码与 Message 的映射

3.2.1 定义错误信息

新建 error_handler.go

package error_handle

import ("github.com/pkg/errors")

// 1、自定义 error 构造体,并重写 Error()办法
// 谬误时返回自定义构造
type CustomError struct {
    Code       int    `json:"code"`    // 业务码
    TagMessage string `json:"message"` // 形容信息
}

func (e *CustomError) Error() string {return e.TagMessage}

// 2、定义 errorCode
const (
    // 服务级错误码
    ServerError        = 10101
    TooManyRequests    = 10102
    ParamBindError     = 10103
    AuthorizationError = 10104
    CallHTTPError      = 10105
    ResubmitError      = 10106
    ResubmitMsg        = 10107
    HashIdsDecodeError = 10108
    SignatureError     = 10109

    // 业务模块级错误码
    // 用户模块
    IllegalUserName = 20101
    UserCreateError = 20102
    UserUpdateError = 20103
    UserSearchError = 20104

    // 受权调用方
    AuthorizedCreateError    = 20201
    AuthorizedListError      = 20202
    AuthorizedDeleteError    = 20203
    AuthorizedUpdateError    = 20204
    AuthorizedDetailError    = 20205
    AuthorizedCreateAPIError = 20206
    AuthorizedListAPIError   = 20207
    AuthorizedDeleteAPIError = 20208

    // 管理员
    AdminCreateError             = 20301
    AdminListError               = 20302
    AdminDeleteError             = 20303
    AdminUpdateError             = 20304
    AdminResetPasswordError      = 20305
    AdminLoginError              = 20306
    AdminLogOutError             = 20307
    AdminModifyPasswordError     = 20308
    AdminModifyPersonalInfoError = 20309

    // 配置
    ConfigEmailError        = 20401
    ConfigSaveError         = 20402
    ConfigRedisConnectError = 20403
    ConfigMySQLConnectError = 20404
    ConfigMySQLInstallError = 20405
    ConfigGoVersionError    = 20406

    // 实用工具箱
    SearchRedisError = 20501
    ClearRedisError  = 20502
    SearchRedisEmpty = 20503
    SearchMySQLError = 20504

    // 菜单栏
    MenuCreateError = 20601
    MenuUpdateError = 20602
    MenuListError   = 20603
    MenuDeleteError = 20604
    MenuDetailError = 20605

    // 借书
    BookNotFoundError        = 20701
    BookHasBeenBorrowedError = 20702
)

// 3、定义 errorCode 对应的文本信息
var codeTag = map[int]string{
    ServerError:        "Internal Server Error",
    TooManyRequests:    "Too Many Requests",
    ParamBindError:     "参数信息有误",
    AuthorizationError: "签名信息有误",
    CallHTTPError:      "调用第三方 HTTP 接口失败",
    ResubmitError:      "Resubmit Error",
    ResubmitMsg:        "请勿反复提交",
    HashIdsDecodeError: "ID 参数有误",
    SignatureError:     "SignatureError",

    IllegalUserName: "非法用户名",
    UserCreateError: "创立用户失败",
    UserUpdateError: "更新用户失败",
    UserSearchError: "查问用户失败",

    AuthorizedCreateError:    "创立调用方失败",
    AuthorizedListError:      "获取调用方列表页失败",
    AuthorizedDeleteError:    "删除调用方失败",
    AuthorizedUpdateError:    "更新调用方失败",
    AuthorizedDetailError:    "获取调用方详情失败",
    AuthorizedCreateAPIError: "创立调用方 API 地址失败",
    AuthorizedListAPIError:   "获取调用方 API 地址列表失败",
    AuthorizedDeleteAPIError: "删除调用方 API 地址失败",

    AdminCreateError:             "创立管理员失败",
    AdminListError:               "获取管理员列表页失败",
    AdminDeleteError:             "删除管理员失败",
    AdminUpdateError:             "更新管理员失败",
    AdminResetPasswordError:      "重置明码失败",
    AdminLoginError:              "登录失败",
    AdminLogOutError:             "退出失败",
    AdminModifyPasswordError:     "批改明码失败",
    AdminModifyPersonalInfoError: "批改个人信息失败",

    ConfigEmailError:        "批改邮箱配置失败",
    ConfigSaveError:         "写入配置文件失败",
    ConfigRedisConnectError: "Redis 连贯失败",
    ConfigMySQLConnectError: "MySQL 连贯失败",
    ConfigMySQLInstallError: "MySQL 初始化数据失败",
    ConfigGoVersionError:    "GoVersion 不满足要求",

    SearchRedisError: "查问 RedisKey 失败",
    ClearRedisError:  "清空 RedisKey 失败",
    SearchRedisEmpty: "查问的 RedisKey 不存在",
    SearchMySQLError: "查问 mysql 失败",

    MenuCreateError: "创立菜单失败",
    MenuUpdateError: "更新菜单失败",
    MenuDeleteError: "删除菜单失败",
    MenuListError:   "获取菜单列表页失败",
    MenuDetailError: "获取菜单详情失败",

    BookNotFoundError:        "书未找到",
    BookHasBeenBorrowedError: "书曾经被借走了",
}

func Text(code int) string {return codeTag}

// 4、新建自定义 error 实例化
func NewCustomError(code int) error {
    // 首次调用得用 Wrap 办法,进行实例化
    return errors.Wrap(&CustomError{
        Code:       code,
        TagMessage: codeTag,
    }, "")
}

3.3 自定义 Error 应用

新建测试文件:error_handler_test.go

package error_handle

import (
    "fmt"
    "github.com/pkg/errors"
    "testing"
)

func TestText(t *testing.T) {books := []string{
        "Book1",
        "Book222222",
        "Book3333333333",
    }

    for _, bookName := range books {err := searchBook(bookName)

        // 非凡业务场景:如果发现书被借走了,下次再来就行了,不须要作为错误处理
        if err != nil {
            // 提取 error 这个 interface 底层的错误码,个别在 API 的返回前才提取
            // As - 获取谬误的具体实现
            var myError = new(CustomError)
            // As - 解析谬误内容
            if errors.As(err, &myError) {fmt.Printf("AS 中的信息:以后书为: %s ,error code is %d, message is %s\n", bookName, myError.Code, myError.TagMessage)
            }

            // 非凡场景,指定谬误 (ErrorBookHasBeenBorrowed) 时,打印即可,不返回谬误
            // Is - 判断谬误是否为指定类型
            if errors.Is(err,  NewCustomError(BookHasBeenBorrowedError)) {fmt.Printf("IS 中的信息:%s 曾经被借走了, 只需按 Info 解决!\n", bookName)
                err = nil
            }else {
                // 如果已有堆栈信息,应调用 WithMessage 办法
                newErr := errors.WithMessage(err, "WithMessage err")
                fmt.Printf("IS 中的信息:%s 未找到,应该按 Error 解决! ,newErr is %s\n", bookName , newErr)
            }
        }
    }
}

func searchBook(bookName string) error {
    // 1 发现图书馆不存在这本书 - 认为是谬误,须要打印具体的错误信息
    if len(bookName) > 10 {return NewCustomError(BookHasBeenBorrowedError)
    } else if len(bookName) > 6 {
        // 2 发现书被借走了 - 打印一下被接走的提醒即可,不认为是谬误
        return NewCustomError(BookHasBeenBorrowedError)
    }
    // 3 找到书 - 不须要任何解决
    return nil
}

3.3 形式二:借助 generate 简化代码(倡议应用)

形式一保护错误码与错误信息的关系较为简单,咱们能够借助 go generate 来主动生成代码。

3.3.1 装置 stringer

stringer 不是 Go 自带工具,须要手动装置。执行如下命令即可

go get golang.org/x/tools/cmd/stringer

3.3.1 定义错误信息

新建 error_handler.go。在 error_handler 中,减少正文 //go:generate stringer -type ErrCode -linecomment。执行 go generate,会生成新的文件

package error_handle

import ("github.com/pkg/errors")

// 1、自定义 error 构造体,并重写 Error()办法
// 谬误时返回自定义构造
type CustomError struct {
    Code    ErrCode `json:"code"`    // 业务码
    Message string  `json:"message"` // 业务码
}

func (e *CustomError) Error() string {return e.Code.String()
}

type ErrCode int64 // 错误码

// 2、定义 errorCode
//go:generate stringer -type ErrCode -linecomment
const (
    // 服务级错误码
    ServerError        ErrCode = 10101 // Internal Server Error
    TooManyRequests    ErrCode = 10102 // Too Many Requests
    ParamBindError     ErrCode = 10103 // 参数信息有误
    AuthorizationError ErrCode = 10104 // 签名信息有误
    CallHTTPError      ErrCode = 10105 // 调用第三方 HTTP 接口失败
    ResubmitError      ErrCode = 10106 // ResubmitError
    ResubmitMsg        ErrCode = 10107 // 请勿反复提交
    HashIdsDecodeError ErrCode = 10108 // ID 参数有误
    SignatureError     ErrCode = 10109 // SignatureError

    // 业务模块级错误码
    // 用户模块
    IllegalUserName ErrCode = 20101 // 非法用户名
    UserCreateError ErrCode = 20102 // 创立用户失败
    UserUpdateError ErrCode = 20103 // 更新用户失败
    UserSearchError ErrCode = 20104 // 查问用户失败

    // 配置
    ConfigEmailError        ErrCode = 20401 // 批改邮箱配置失败
    ConfigSaveError         ErrCode = 20402 // 写入配置文件失败
    ConfigRedisConnectError ErrCode = 20403 // Redis 连贯失败
    ConfigMySQLConnectError ErrCode = 20404 // MySQL 连贯失败
    ConfigMySQLInstallError ErrCode = 20405 // MySQL 初始化数据失败
    ConfigGoVersionError    ErrCode = 20406 // GoVersion 不满足要求

    // 实用工具箱
    SearchRedisError ErrCode = 20501 // 查问 RedisKey 失败
    ClearRedisError  ErrCode = 20502 // 清空 RedisKey 失败
    SearchRedisEmpty ErrCode = 20503 // 查问的 RedisKey 不存在
    SearchMySQLError ErrCode = 20504 // 查问 mysql 失败

    // 菜单栏
    MenuCreateError ErrCode = 20601 // 创立菜单失败
    MenuUpdateError ErrCode = 20602 // 更新菜单失败
    MenuListError   ErrCode = 20603 // 删除菜单失败
    MenuDeleteError ErrCode = 20604 // 获取菜单列表页失败
    MenuDetailError ErrCode = 20605 // 获取菜单详情失败

    // 借书
    BookNotFoundError        ErrCode = 20701 // 书未找到
    BookHasBeenBorrowedError ErrCode = 20702 // 书曾经被借走了
)

// 4、新建自定义 error 实例化
func NewCustomError(code ErrCode) error {
    // 首次调用得用 Wrap 办法,进行实例化
    return errors.Wrap(&CustomError{
        Code:    code,
        Message: code.String(),}, "")
}

3.3.2 自定义 Error 应用

新建测试文件:error_handler_test.go

package error_handle

import (
    "fmt"
    "github.com/pkg/errors"
    "testing"
)

func TestText(t *testing.T) {books := []string{
        "Book1",
        "Book222222",
        "Book3333333333",
    }

    for _, bookName := range books {err := searchBook(bookName)

        // 非凡业务场景:如果发现书被借走了,下次再来就行了,不须要作为错误处理
        if err != nil {
            // 提取 error 这个 interface 底层的错误码,个别在 API 的返回前才提取
            // As - 获取谬误的具体实现
            var customErr = new(CustomError)
            // As - 解析谬误内容
            if errors.As(err, &customErr) {//fmt.Printf("AS 中的信息:以后书为: %s ,error code is %d, message is %s\n", bookName, customErr.Code, customErr.Message)
                if customErr.Code == BookHasBeenBorrowedError {fmt.Printf("IS 中的 info 信息:%s 曾经被借走了, 只需按 Info 解决!\n", bookName)
                } else {
                    // 如果已有堆栈信息,应调用 WithMessage 办法
                    newErr := errors.WithMessage(err, "WithMessage err1")
                    // 应用 %+ v 能够打印残缺的堆栈信息
                    fmt.Printf("IS 中的 error 信息:%s 未找到,应该按 Error 解决! ,newErr is: %+v\n", bookName, newErr)
                }
            }
        }
    }
}

func searchBook(bookName string) error {
    // 1 发现图书馆不存在这本书 - 认为是谬误,须要打印具体的错误信息
    if len(bookName) > 10 {return NewCustomError(BookNotFoundError)
    } else if len(bookName) > 6 {
        // 2 发现书被借走了 - 打印一下被接走的提醒即可,不认为是谬误
        return NewCustomError(BookHasBeenBorrowedError)
    }
    // 3 找到书 - 不须要任何解决
    return nil
}

4 总结

  1. CustomError 作为全局 error 的底层实现,保留具体的错误码和错误信息;
  2. CustomError向上返回谬误时,第一次先用 Wrap 初始化堆栈,后续用 WithMessage 减少堆栈信息;
  3. error 中解析具体谬误时,用 errors.As 提取出CustomError,其中的错误码和错误信息能够传入到具体的 API 接口中;
  4. 要判断 error 是否为指定的谬误时,用 errors.Is + Handler Error 的办法,解决一些特定状况下的逻辑;

Tips:

  1. 不要始终用 errors.Wrap 来重复包装谬误,堆栈信息会爆炸,具体情况可自行测试理解
  2. 利用 go generate 能够大量简化初始化 Erro 反复的工作
  3. github.com/pkg/errors和规范库的 error 齐全兼容,能够先替换、后续革新历史遗留的代码
  4. 肯定要留神打印 error 的堆栈须要用 %+v,而原来的%v 仍旧为一般字符串办法;同时也要留神日志采集工具是否反对多行匹配

我是简凡,一个励志用最简略的语言,形容最简单问题的新时代农民工。求点赞,求关注,如果你对此篇文章有什么纳闷,欢送在我的微信公众号中留言,我还能够为你提供以下帮忙:

  • 帮忙建设本人的常识体系
  • 互联网实在高并发场景实战解说
  • 不定期分享 Golang、Java 相干业内的经典场景实际

我的博客:https://besthpt.github.io/
微信公众号:

退出移动版