如何设计errors处理包
使用pkg/errors 打印调用栈
go自带的errors包不能打印出调用栈,而pkg/errors包可以 。
1package main
2
3import (
4 "errors"
5 "fmt"
6 pkgerros "github.com/pkg/errors"
7)
8
9func Div(a, b int) (int, error) {
10 if b == 0 {
11 return 0, errors.New("b is zero")
12 }
13 return a / b, nil
14}
15
16func PkgErrorDiv(a, b int) (int, error) {
17 if b == 0 {
18 return 0, pkgerros.New("b is zero")
19 }
20 return a / b, nil
21}
22
23func main() {
24 _, err := Div(2, 0)
25 fmt.Printf("%+v\n", err)
26 fmt.Println("--------")
27 _, err = PkgErrorDiv(2, 0)
28 fmt.Printf("%+v", err)
29}
result:
1➜ bdsp git:(mao_main) ✗ go run test2.go
2b is zero
3--------
4b is zero
5main.PkgErrorDiv
6 /Users/maozhongyu/code/bdsp/test2.go:18
7main.main
8 /Users/maozhongyu/code/bdsp/test2.go:27
9runtime.main
10 /Users/maozhongyu/go1.18.5/src/runtime/proc.go:250
11runtime.goexit
12 /Users/maozhongyu/go1.18.5/src/runtime/asm_amd64.s:1571%
pkg/errors 错误栈、和提示信息的打印
Warp()、WithMessage,WithStack
Wrap()函数在已有错误基础上同时附加堆栈信息和新提示信息
WithMessage() 函数在已有错误基础上附加新提示信息
WithStack() 函数在已有错误基础上附加堆栈信息。在实际情况下根据选择使用
1package main
2
3import (
4 "fmt"
5 "github.com/pkg/errors"
6)
7
8func Service(name string) error {
9 if name == "" {
10 return errors.New("name is empty")
11 }
12 return nil
13}
14
15func Controller() error {
16 err := Service("")
17 if err != nil {
18 return errors.Wrap(err, "Service has error")
19 }
20 return nil
21}
22
23func main() {
24 err := Controller()
25 fmt.Printf("%+v", err)
26}
result
1➜ bdsp git:(mao_main) ✗ go run test2.go
2name is empty
3main.Service
4 /Users/maozhongyu/code/bdsp/test2.go:10
5main.Controller
6 /Users/maozhongyu/code/bdsp/test2.go:16
7main.main
8 /Users/maozhongyu/code/bdsp/test2.go:24
9runtime.main
10 /Users/maozhongyu/go1.18.5/src/runtime/proc.go:250
11runtime.goexit
12 /Users/maozhongyu/go1.18.5/src/runtime/asm_amd64.s:1571
13get has error
14main.Controller
15 /Users/maozhongyu/code/bdsp/test2.go:18
16main.main
17 /Users/maozhongyu/code/bdsp/test2.go:24
18runtime.main
19 /Users/maozhongyu/go1.18.5/src/runtime/proc.go:250
20runtime.goexit
21 /Users/maozhongyu/go1.18.5/src/runtime/asm_amd64.s:1571% ➜ bdsp git:(mao_main) ✗ go run test2.go
22name is empty
23main.Service
24 /Users/maozhongyu/code/bdsp/test2.go:10
25main.Controller
26 /Users/maozhongyu/code/bdsp/test2.go:16
27main.main
28 /Users/maozhongyu/code/bdsp/test2.go:24
29runtime.main
30 /Users/maozhongyu/go1.18.5/src/runtime/proc.go:250
31runtime.goexit
32 /Users/maozhongyu/go1.18.5/src/runtime/asm_amd64.s:1571
33Service has error
34main.Controller
35 /Users/maozhongyu/code/bdsp/test2.go:18
36main.main
37 /Users/maozhongyu/code/bdsp/test2.go:24
38runtime.main
39 /Users/maozhongyu/go1.18.5/src/runtime/proc.go:250
40runtime.goexit
41 /Users/maozhongyu/go1.18.5/src/runtime/asm_amd64.s:1571%
is和as方法判断错误
golang 1.13 新增 Is()
函数,它可以顺着错误链(error chain)上所有被包装的错误(wrapped error)的值做比较,直到找到一个匹配的错误。
1package main
2
3import (
4 "fmt"
5 "github.com/pkg/errors"
6
7 e "errors"
8)
9
10var myerror = errors.New("name is empty")
11
12func Service(name string) error {
13 if name == "" {
14 return myerror
15 }
16 return nil
17}
18
19func Controller() error {
20 err := Service("")
21 if err != nil {
22 return errors.Wrap(err, "Service has error")
23 }
24 return nil
25}
26
27func main() {
28 err := Controller()
29 //fmt.Printf("%+v", err)
30 // if errors.Is(err, myerror) {
31 if e.Is(err, myerror) {
32 fmt.Println("is myerror value")
33 }
34}
在 golang 1.13 中,新增 As()
函数,当 error 类型的变量是一个包装错误(wrap error)时,它可以顺着错误链(error chain)上所有被包装的错误(wrapped error)的类型做比较,直到找到一个匹配的错误类型,并返回 true,如果找不到,则返回 false。
通常,我们会使用 As()
函数判断一个 error 类型的变量是否为特定的自定义错误类型。
1package main
2
3import (
4 "fmt"
5 "github.com/pkg/errors"
6)
7
8// 自定义的错误类型
9type DefineError struct {
10 msg string
11}
12
13func (d *DefineError) Error() string {
14 return d.msg
15}
16
17func Service(name string) error {
18 if name == "" {
19 return &DefineError{msg: "error 1"}
20 }
21 return nil
22}
23
24func Controller() error {
25 err := Service("")
26 if err != nil {
27 return errors.Wrap(err, "Service has error")
28 }
29 return nil
30}
31
32func main() {
33 err := Controller()
34 //fmt.Printf("%+v", err)
35 // if errors.Is(err, myerror) {
36
37 var myerror *DefineError
38 if errors.As(err, &myerror) {
39 fmt.Println("AS error")
40 }
41}
http code 响应
方案1 : 全部返回200,facebook,通过业务code区分,但是这种如prometheus不兼容
方案2: 业务code 转换为 http code 。
如何设计错误码
code 设计思路
- code 不能和http的code,通过这些code 可以看出来错误码来自哪个项目,哪个模块
- 请求错误了,我们不要去通过code判断,应该通过http的code来判断
- 最好是能有文档对每个错误码描述,手动维护还是自动维护
- 错误码可以直接返回给前端, msg这种不能包含敏感信息。
错误码设计的重要性
- 给程序员看的,什么错误码就知道哪里错了。
- http状态码不够。
1001001
10前2位: 表示服务
01: 代表某个服务下的模块 ,00 代表通用错误,如数据库,redis错误
001: 如用户查不到,每个模块下支持1000个错误码
错误码,映射成http状态码。
grpc中的error处理
client 可以拿到具体serve的error code值。
server:
1package helloworld
2
3import (
4 "context"
5 "google.golang.org/grpc/codes"
6 "google.golang.org/grpc/status"
7)
8
9type Binding struct {
10 UnimplementedGreeterServer
11}
12
13func (s *Binding) SayHello(ctx context.Context, r *HelloRequest) (*HelloResponse, error) {
14
15 return nil, status.Error(codes.NotFound, " HELLO NOT FOUND")
16
17 return &HelloResponse{
18 Message: "hello " + r.Name,
19 }, nil
20}
client:
1package client
2
3import (
4 "context"
5 "crgo/grpc/biz/helloworld"
6 "fmt"
7 "google.golang.org/grpc"
8 "google.golang.org/grpc/status"
9 "log"
10)
11
12func SayHello(client helloworld.GreeterClient) error {
13 resp, err := client.SayHello(context.Background(), &helloworld.HelloRequest{
14 Name: "crmao",
15 })
16 if err != nil {
17
18 s, ok := status.FromError(err)
19 if !ok {
20 fmt.Println("is not stardand grpc error")
21 return err
22 }
23 fmt.Println(s.Code())
24
25 return err
26 }
27 log.Printf("client.Sayhello resp:%s", resp.Message)
28 return nil
29}
30
31func Do() error {
32 conn, err := grpc.Dial(":8081", grpc.WithInsecure())
33 if err != nil {
34 log.Fatalf("client conn err :%v", err)
35 return err
36 }
37 defer conn.Close()
38 client := helloworld.NewGreeterClient(conn)
39 if err := SayHello(client); err != nil {
40 log.Fatalf("sayhello err :%v", err)
41 return err
42 }
43 return nil
44}
error实现withcode模式
error带上自定义带code
核心思路:
首先注册code 到全局 error中,一个自定义code 对应一个http状态code 。
1package code
2
3import (
4 "gotool/pkg/errors"
5 "net/http"
6
7 "github.com/novalagung/gubrak"
8)
9
10type ErrCode struct {
11 //错误码
12 C int
13
14 //http的状态码
15 HTTP int
16
17 //扩展字段
18 Ext string
19
20 //引用文档
21 Ref string
22}
23
24func (e ErrCode) HTTPStatus() int {
25 return e.HTTP
26}
27
28func (e ErrCode) String() string {
29 return e.Ext
30}
31
32func (e ErrCode) Reference() string {
33 return e.Ref
34}
35
36func (e ErrCode) Code() int {
37 if e.C == 0 {
38 return http.StatusInternalServerError
39 }
40 return e.C
41}
42
43func register(code int, httpStatus int, message string, refs ...string) {
44 found, _ := gubrak.Includes([]int{200, 400, 401, 403, 404, 500}, httpStatus)
45 if !found {
46 panic("http code not in `200, 400, 401, 403, 404, 500`")
47 }
48 var ref string
49 if len(refs) > 0 {
50 ref = refs[0]
51 }
52 coder := ErrCode{
53 C: code,
54 HTTP: httpStatus,
55 Ext: message,
56 Ref: ref,
57 }
58
59 errors.MustRegister(coder)
60}
61
62var _ errors.Coder = (*ErrCode)(nil)
pkg/errors 注册code函数
1// codes contains a map of error codes to metadata.
2var codes = map[int]Coder{}
3var codeMux = &sync.Mutex{}
4// MustRegister register a user define error code.
5// It will panic when the same Code already exist.
6func MustRegister(coder Coder) {
7 if coder.Code() == 0 {
8 panic("code '0' is as ErrUnknown error code")
9 }
10
11 codeMux.Lock()
12 defer codeMux.Unlock()
13
14 if _, ok := codes[coder.Code()]; ok {
15 panic(fmt.Sprintf("code: %d already exist", coder.Code()))
16 }
17
18 codes[coder.Code()] = coder
19}
WithCode error函数, 响应函数、 解析函数
1func WithCode(code int, format string, args ...interface{}) error {
2 return &withCode{
3 err: fmt.Errorf(format, args...),
4 code: code,
5 stack: callers(),
6 }
7}
8// WriteResponse write an error or the response data into http response body.
9// It use errors.ParseCoder to parse any error into errors.Coder
10// errors.Coder contains error code, user-safe error message and http status code.
11func WriteResponse(c *gin.Context, err error, data interface{}) {
12 if err != nil {
13 errStr := fmt.Sprintf("%#+v", err)
14 coder := errors.ParseCoder(err)
15 c.JSON(coder.HTTPStatus(), ErrResponse{
16 Code: coder.Code(),
17 Message: coder.String(),
18 Detail: errStr,
19 Reference: coder.Reference(),
20 })
21
22 return
23 }
24
25 c.JSON(http.StatusOK, data)
26}
27
28//解析函数
29func ParseCoder(err error) Coder {
30 if err == nil {
31 return nil
32 }
33
34 if v, ok := err.(*withCode); ok {
35 if coder, ok := codes[v.code]; ok {
36 return coder
37 }
38 }
39 return unknownCoder
40}
内部错误转化grpc error
我们有自定义的错误,错误code, 但是这个不是grpc error 。
代码见:
注册code (关联了http code ) 变成 error withcode , https://github.com/cr-mao/crgo/blob/main/cmd/grpc_client.go
服务端代码: https://github.com/cr-mao/crgo/blob/main/grpc/biz/helloworld/grpc.go
客户端代码: https://github.com/cr-mao/crgo/blob/main/grpc/client/client.go
code 库: https://github.com/cr-mao/crgo/tree/main/infra/code
错误包: https://github.com/cr-mao/crgo/tree/main/infra/errors
kratos的错误其实也是有一套自己的内部错误和grpc 错误的转换。