grpc error code是有限的,并不能cover用户需求,因此自定义error,结合grpc 提供的接口进行扩展,下面是一些简单的代码实践。
代码目录:
$GOPATH/src/test/utils
子目录 mysqlerrors (test/utils/mysqlerrors):
error_codes.go
package mysqlerrors
const ( // See https://dev.mysql.com/doc/refman/5.7/en/error-messages-server.html
errDuplicateEntry = 1062 // ER_DUP_ENTRY
errNoReferencedRow2 = 1452 // ER_NO_REFERENCED_ROW_2
)
errors.go
package mysqlerrors
// recordNotUniqueError represents RecordNotUnique error
type recordNotUniqueError struct {
errerror
}
// Error implements error interface
func (e *recordNotUniqueError) Error() string {
return e.err.Error()
}
// Cause implements this interface
//
// type causer interface {
// Cause() error
// }
//
func (e *recordNotUniqueError) Cause() error {
return e.err
}
// RecordNotUnique implements RecordNotUnique interface
func (e *recordNotUniqueError) RecordNotUnique() {}
// invalidForeignKeyError represents InvalidForeignKey error
type invalidForeignKeyError struct {
errerror
}
// Error implements error interface
func (e *invalidForeignKeyError) Error() string {
return e.err.Error()
}
// Cause implements this interface
//
// type causer interface {
// Cause() error
// }
//
func (e *invalidForeignKeyError) Cause() error {
return e.err
}
// InvalidForeignKey implements InvalidForeignKey interface
func (e *invalidForeignKeyError) InvalidForeignKey() {}
get_mysql_error_code.go
package mysqlerrors
import (
"strconv"
"strings"
)
// getMysqlErrorCode get the mysql error code from a *mysql.MySQLError type error
// if error code found, returns code and true. Otherwise, returns 0 and false.
func getMysqlErrorCode(err error) (int, bool) {
// as https://github.com/go-sql-driver/mysql/blob/master/errors.go#L64
codeStr := strings.Split(strings.TrimPrefix(err.Error(),"Error "), ":")[0]
if code, err := strconv.Atoi(codeStr); err == nil {
return code, true
}
return 0, false
}
to_test_error.go
package mysqlerrors
import (
"database/sql"
"errors"
"pkg/testerrors"
perrors"pkg/errors"
"proto"
"status"
gstatus"google.golang.org/grpc/status"
)
// ToTestError translate a general error to Test error.
// See package "pkg/testerrors"
func ToTestError(err error) error {
code, ok := getMysqlErrorCode(perrors.Cause(err))
if !ok {
returnerr
}
switch code {
default:
returnerr
case errDuplicateEntry:
return &recordNotUniqueError{err}
case errNoReferencedRow2:
return &invalidForeignKeyError{err}
}
}
// ToGrpcErrFromTestErr translate Test error to Test grpc error.
// See package "pkg/testerrors"
func ToGrpcErrFromTestErr(err error) error {
if err == nil {
return err
}
switch err.(type) {
default:
return err
case testerrors.RecordNotUnique:
return status.Error(proto.CODE_TEST_ERR_DUPLICATE_ENTRY, err.Error())
case testerrors.InvalidForeignKey:
return status.Error(proto.CODE_TEST_ERR_NO_REFERENCED_ROW2, err.Error())
}
}
// ToTestErrFromGrpcErr translate grpc error to Test error.
// See package "pkg/testerrors"
func ToTestErrFromGrpcErr(err error) error {
if err == nil {
return err
}
s, ok := gstatus.FromError(err)
if !ok {
return errors.New("not a grpc error")
}
details := s.Details()
var testError *proto.StatusList
if len(details) == 0 {
return err
}
testError, ok = s.Details()[0].(*proto.StatusList)
if !ok {
return err
}
if len(testError.Errors) == 0 {
return nil
}
code := testError.Errors[0].Code
switch code {
default:
return err
case proto.CODE_TEST_ERR_DUPLICATE_ENTRY:
return &recordNotUniqueError{err}
case proto.CODE_TEST_ERR_NO_REFERENCED_ROW2:
return &invalidForeignKeyError{err}
}
}
子目录pkg (test/utils/pkg):
pkg/errors/cause.go
package errors
// Cause is copied from https://github.com/pkg/errors/blob/master/errors.go
func Cause(err error) error {
type causer interface {
Cause()error
}
for err != nil {
cause, ok := err.(causer)
if !ok {
break
}
err = cause.Cause()
}
returnerr
}
pkg/testerrors/errors.go
// Package testerrors provides basic interfaces to Test execution errors.
package testerrors
// RecordNotUnique returned when a record cannot be inserted or updated because it would violate a uniqueness constraint.
type RecordNotUnique interface {
RecordNotUnique()
}
// InvalidForeignKey returned when a record cannot be inserted or updated because it references a non-existent record.
type InvalidForeignKey interface {
InvalidForeignKey()
}
子目录proto ():
test/utils/proto/status.proto
syntax = "proto3";
package proto;
enum CODE {
UNKNOWN =0; // http code 500
//Test duplicate entry error
TEST_ERR_DUPLICATE_ENTRY =3600;
//Test no referened row error
TEST_ERR_NO_REFERENCED_ROW2 =3601;
}
message Status {
CODE code =1;
string field = 2;
string message = 3;
string detail = 4;
}
message StatusList {
repeated Status errors = 1;
}
protoc -I=/usr/local/include -I=. --go_out=. status.proto
子目录status:
test/utils/status/status.go
package status
import (
...
"google.golang.org/grpc/codes"
"google.golang.org/grpc/grpclog"
"google.golang.org/grpc/status"
)
type Detail map[string]interface{}
type Status struct {
Code proto.CODE
Field string
Messagestring
Detail Detail
}
// Error builds a single error with code and message.
func Error(code proto.CODE, message string) error {
return errorsWith(grpcCode(code), message, &Status{Code: code, Message: message})
}
func buildMsg(errorList []*Status) (msg string) {
for _, err := range errorList {
msg += err.Message +";"
}
return
}
// Errors builds an error with a grpc Status code and a list of Status.
// The value to "Detail" in each Status MUST be a JSON object.
func errorsWith(c codes.Code, msg string, errorList ...*Status) error {
protoStatusList := asProtoStatus(errorList)
if len(protoStatusList) == 0 {
protoStatusList =append(protoStatusList, &proto.Status{Code: proto.CODE_UNKNOWN, Detail: "{}"})
}
s, err := status.New(c, msg).WithDetails(&proto.StatusList{
Errors: protoStatusList,
})
if err != nil {
grpclog.Print("Error error:", err)
return err
}
return s.Err()
}
func asProtoStatus(errorList []*Status) []*proto.Status {
list :=make([]*proto.Status, 0, len(errorList))
for _, err := range errorList {
detailStr :="{}"
if err.Detail != nil {
detailBytes, e := json.Marshal(err.Detail)
if e == nil {
detailStr =string(detailBytes)
}
}
list =append(list, &proto.Status{
Code: err.Code,
Field: err.Field,
Message: err.Message,
Detail: detailStr,
})
}
return list
}
func grpcCode(code proto.CODE) codes.Code {
c, ok := grpcCodeMap[code]
if ok {
return c
}
return codes.Unknown
}
func grpcCodeFrom(errorList ...*Status) codes.Code {
last := codes.Unknown
for i, e := range errorList {
if i == 0 {
last = grpcCode(e.Code)
continue
}
if last != grpcCode(e.Code) {
return codes.Unknown
}
}
return last
}
var grpcCodeMap = map[proto.CODE]codes.Code{
proto.CODE_UNKNOWN: codes.Unknown,
}