如何设计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 设计思路

  1. code 不能和http的code,通过这些code 可以看出来错误码来自哪个项目,哪个模块
  2. 请求错误了,我们不要去通过code判断,应该通过http的code来判断
  3. 最好是能有文档对每个错误码描述,手动维护还是自动维护
  4. 错误码可以直接返回给前端, 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 错误的转换。