写给go开发者的Tars教程-错误处理

2024-01-08
4分钟阅读时长

本篇为【写给go开发者的Tars教程】系列第四篇

第一篇:Tars协议基础

第二篇:通信模式

第三篇:拦截器

第四篇:错误处理

本系列将持续更新,欢迎关注👏获取实时通知

基本错误处理

首先回顾下tars文件和生成出来的client与server端的接口

module order {
    struct Order {
        1 require string id;
        2 optional vector<string> items;
        3 optional string description;
        4 require float price;
        5 optional string destination;
    };
    
    interface OrderManagement {
        Order getOrder(string orderId);
    };
};
type OrderManagement struct {
	servant m.Servant
}

func (obj *OrderManagement) GetOrder(orderId string, opts ...map[string]string) (Order, error) {
	return obj.GetOrderWithContext(context.Background(), orderId, opts...)
}

func (obj *OrderManagement) GetOrderWithContext(tarsCtx context.Context, orderId string, opts ...map[string]string) (ret Order, err error) {
	// ......
	return ret, nil
}

func (obj *OrderManagement) GetOrderOneWayWithContext(tarsCtx context.Context, orderId string, opts ...map[string]string) (ret Order, err error) {
	// ......
	return ret, nil
}
type OrderManagementServant interface {
	GetOrder(orderId string) (ret Order, err error)
}
type OrderManagementServantWithContext interface {
	GetOrder(tarsCtx context.Context, orderId string) (ret Order, err error)
}

可以看到,虽然我们没有在tars文件中的接口定义设置error返回值,但生成出来的go代码是包含error返回值。

这非常符合Go语言的使用习惯:通常情况下我们定义多个error变量,并且在函数内返回,调用方可以使用errors.Is()或者errors.As()对函数的error进行判断

var (
	ParamsErr = errors.New("params err")
	BizErr    = errors.New("biz err")
)

func Invoke(i bool) error {
	if i {
		return ParamsErr
	} else {
		return BizErr
	}
}

func TestError(t *testing.T) {
	err := Invoke(true)
	if err != nil {
		switch {
		case errors.Is(err, ParamsErr):
			log.Println("params error")
		case errors.Is(err, BizErr):
			log.Println("biz error")
		}
	}
}

🌿 但,在RPC场景下,我们还能进行error的值判断么?

// errors/errors.go
var ParamsErr = errors.New("params err")
// internal/servant/order.go
func (o *OrderCtx) GetOrder(tarsCtx context.Context, orderId string) (ret order.Order, err error) {
	return ret, errors.ParamsErr
}
// errors/errors_test.go
func TestRPCErrors(t *testing.T) {
	comm := tars.GetCommunicator()
	client := new(order.OrderManagement)
	obj := "Test.OrderServer.OrderObj@tcp -h 127.0.0.1 -p 8080 -t 60000"
	comm.StringToProxy(obj, client)

	retrievedOrder, err := client.GetOrderWithContext(context.Background(), "1")
	if err != nil && errors.Is(err, ParamsErr) {
		// 不会走到这里,因为err和ParamsErr不相等
		t.Fatal(err)
	}
	t.Logf("order: %+v", retrievedOrder)
}

很明显,serverclient并不在同一个进程甚至都不在同一个台机器上,所以errors.Is()或者errors.As()是没有办法做这样判断的

业务错误码

那么如何做?在http的服务中,我们会使用错误码的方式来区分不同错误,通过判断errno来区分不同错误

{
    "errno": 0,
    "msg": "ok",
    "data": {}
}

{
    "errno": 1000,
    "msg": "params error",
    "data": {}
}

类似的,我们调整下我们tars定义:在返回值里携带错误信息

module order {
    struct Order {
        1 require string id;
        2 optional vector<string> items;
        3 optional string description;
        4 require float price;
        5 optional string destination;
    };
    enum BizErrno {
        Ok = 0,
        ParamsErr = 1,
        BizErr = 2,
    };
    struct GetOrderResp {
        1 require BizErrno errno;
        2 optional string msg;
        3 optional Order data;
    };
    interface OrderManagement {
        GetOrderResp getOrder(string orderId);
    };
};

于是在服务端实现的时候,我们可以返回对应数据或者错误状态码

func (o *OrderCtx) GetOrder(tarsCtx context.Context, orderId string) (ret order.GetOrderResp, err error) {
	ord, exists := orders[orderId]
	if exists {
		return order.GetOrderResp{
			Errno: order.BizErrno_Ok,
			Msg:   "OK",
			Data:  ord,
		}, nil
	}

	return order.GetOrderResp{
		Errno: order.BizErrno_ParamsErr,
		Msg:   "Order does not exist",
	}, nil
}

在客户端可以判断返回值的错误码来区分错误,这是我们在常规RPC的常见做法

// Get Order
resp, err := client.GetOrderWithContext(context.Background(), "1")
if err != nil {
    t.Fatal(err)
}
if resp.Errno != order.BizErrno_Ok {
    t.Fatal(resp.Msg)
}
t.Logf("GetOrder Response -> : %+v", resp.Data)

🌿 但,这么做有什么问题么?

很明显,对于clinet侧来说,本身就可能遇到网络失败等错误,所以返回值(ret order.GetOrderResp, err error)包含error并不会非常突兀

但再看一眼server侧的实现,我们把错误枚举放在GetOrderResp中,此时返回的另一个error就变得非常尴尬了,该继续返回一个error呢,还是直接都返回nil呢?两者的功能极度重合

那有什么办法既能利用上error这个返回值,又能让client端枚举出不同错误么?一个非常直观的想法:让error里记录枚举值就可以了!

但我们都知道Go里的error是只有一个string的,可以携带的信息相当有限,如何传递足够多的信息呢?TarsGo官方提供了github.com/TarsCloud/TarsGo/tars.Error的解决方案

使用 tars.Error处理错误

TarsGo 提供了github.com/TarsCloud/TarsGo/tars.Error来表示错误,这个结构包含了 CodeMessage 两个字段

🌲 code是类似于http status code的一系列错误类型的枚举,所有语言 sdk 都会内置这个枚举列表

虽然总共预定义了16个code,但TarsGo框架并没有使用这些code,框架底层定义了自己的错误码。

CodeNumberDescription
OK0成功
CANCELLED1调用取消
UNKNOWN2未知错误

🌲 message就是服务端需要告知客户端的一些错误详情信息

func TestTarsError(t *testing.T) {
	ok := tars.Errorf(basef.TARSSERVERSUCCESS, "ok")
	fmt.Println(ok)

	serverNoFuncErr := tars.Errorf(basef.TARSSERVERNOFUNCERR, "服务器端没有该函数")
	fmt.Println(serverNoFuncErr)
}

tars.Error 和语言 Error 的互转

上文提到无论是serverclient返回的都是errorTarsGo框架提供的tars.Error已经实现了error接口。

所以在服务端可以利用tars.Error并返回

func (o *OrderCtx) GetOrder(tarsCtx context.Context, orderId string) (ret order.Order, err error) {
	ord, exists := orders[orderId]
	if exists {
		return ord, nil
	}

	return ret, tars.Errorf(http.StatusNotFound, "Order does not exist: %v", orderId)
}

到客户端这里我们再利用errors.As(err, &tarsErr)error转回tars.Error

// Get Order
ord, err := client.GetOrderWithContext(context.Background(), "2")
if err != nil {
    var tarsErr = new(tars.Error)
    if ok := errors.As(err, &tarsErr); ok && tarsErr.Code == http.StatusNotFound {
        t.Logf("code: %d, msg: %s", tarsErr.Code, tarsErr.Message)
    } else {
        t.Fatal(err)
    }
    return
}
t.Logf("GetOrder Response -> : %+v", ord)

Tars框架层错误码

module basef
{
    ////////////////////////////////////////////////////////////////
    // TARS定义的返回码
    const int TARSSERVERSUCCESS       = 0;       //服务器端处理成功
    const int TARSSERVERDECODEERR     = -1;      //服务器端解码异常
    const int TARSSERVERENCODEERR     = -2;      //服务器端编码异常
    const int TARSSERVERNOFUNCERR     = -3;      //服务器端没有该函数
    const int TARSSERVERNOSERVANTERR  = -4;      //服务器端没有该Servant对象
    const int TARSSERVERRESETGRID     = -5;      //服务器端灰度状态不一致
    const int TARSSERVERQUEUETIMEOUT  = -6;      //服务器队列超过限制
    const int TARSASYNCCALLTIMEOUT    = -7;      //异步调用超时
    const int TARSINVOKETIMEOUT       = -7;      //调用超时
    const int TARSPROXYCONNECTERR     = -8;      //proxy链接异常
    const int TARSSERVEROVERLOAD      = -9;      //服务器端超负载,超过队列长度
    const int TARSADAPTERNULL         = -10;     //客户端选路为空,服务不存在或者所有服务down掉了
    const int TARSINVOKEBYINVALIDESET = -11;     //客户端按set规则调用非法
    const int TARSCLIENTDECODEERR     = -12;     //客户端解码异常
    const int TARSSENDREQUESTERR      = -13;     //发送出错
    const int TARSSERVERUNKNOWNERR    = -99;     //服务器端位置异常
};

排除框架层的错误码,我们可以定义无数的业务层错误码,结合tars.Error进行合理的业务错误处理。

总结

我们先介绍了TarsGo最基本的错误处理方式:返回error

之后我们又介绍了一种能够携带更多错误信息的方式:tars.Error,它包含CodeMessage等信息,通过tars.Error实现了error接口可以直接用来传输错误。

关注公众号获得更多精彩文章

公众号:程序员大兵