Go 语言中使用 error 类型来表示函数调用或操作返回的错误。当一个函数返回一个 error 类型的值时,调用方必须检查这个值是否为 nil,如果不是,则意味着函数调用或操作发生了错误,调用方需要处理这个错误。
与其他语言的 try-catch 机制不同,Go 语言中的错误处理机制是基于函数返回值的。当函数调用或操作发生错误时,它会返回一个非 nil 的 error 值,然后调用方可以根据这个值来决定如何处理错误。
相比之下,try-catch 机制是基于异常处理的。在其他语言中,当一个函数调用或操作发生异常时,程序会抛出一个异常,然后尝试捕获这个异常并进行处理。
因为异常处理机制可以跨越多个函数调用层次,所以在某些情况下可能更加方便。但是,在其他语言中,异常处理机制也可能会导致代码复杂性增加,并且在某些情况下可能会影响程序的性能。
相比之下,Go 语言中的错误处理机制更加简单,且对代码的影响较小。此外,Go 语言中的错误处理机制可以促使程序员编写更健壮、更可靠的代码,因为错误处理是必须显式处理的。
在 Python 中,我们通常使用 try-except 语句来捕获和处理异常。
例如,假设我们有一个函数 divide(a, b) 用于计算两个数的商,但是如果 b 等于 0,它将抛出一个 ZeroDivisionError 异常。
以下是一个处理 ZeroDivisionError 异常的示例:
def divide(a, b):try:return a / bexcept ZeroDivisionError:print("Error: division by zero")return None
在这个例子中,我们使用 try-except 语句来捕获可能抛出的 ZeroDivisionError 异常。如果异常发生了,我们打印一个错误消息并返回 None。否则,我们返回两个数的商。
在这个例子中,与 Go 语言的错误处理机制类似,我们在函数中显式地处理可能发生的错误,而不是简单地抛出一个异常并期望调用方来处理它。这种方法可以使代码更加健壮,并且可以让程序员更好地控制程序的执行流程。
go语言处理error:
假设我们有一个函数 Divide(a, b) 用于计算两个数的商,但是如果 b 等于 0,它将返回一个错误。
以下是一个处理错误的示例:
func Divide(a, b float64) (float64, error) {if b == 0 {return 0, fmt.Errorf("division by zero")}return a / b, nil
}
在这个例子中,我们使用一个 if 语句来检查 b 是否等于 0。如果 b 等于 0,我们返回一个包含错误消息的 error 类型值,否则,我们返回两个数的商和一个 nil 错误。
在调用方使用该函数时,我们可以通过检查返回的错误值是否为 nil 来确定函数是否成功执行。例如:
result, err := Divide(10, 2)
if err != nil {// 处理错误
}
fmt.Println(result)
在这个例子中,如果 Divide 函数执行成功,则返回的错误值将为 nil,否则我们将处理错误。这种错误处理机制可以使程序员更加容易地编写健壮的代码,并且可以让程序更好地控制执行流程。
errors
包是 Go 语言标准库中的一个简单的错误处理包,它提供了一个 New 函数,可以用于创建一个新的 error 类型的值。例如:err := errors.New("something went wrong")
fmt
包是 Go 语言标准库中的一个格式化包,它也提供了一个 Errorf 函数,可以用于创建一个新的 error 类型的值。例如:err := fmt.Errorf("something went wrong: %s", message)
pkg/errors
包是一个第三方的错误处理包,它提供了更丰富的错误处理功能,包括堆栈跟踪和错误链。例如:err := pkgerrors.Wrap(someError, "something went wrong")
这个例子中,使用 Wrap
函数将 someError
错误包装在一个新的 error 类型值中,并且添加了一个错误信息 “something went wrong”。这个新的 error 类型值还包含了堆栈跟踪和错误链信息,可以帮助我们更好地理解错误的来源。
总之,这些错误处理包都提供了创建和处理错误的基本功能,选择哪个包取决于项目的具体需求和开发者的偏好。
error类型是一个内置的接口类型,用于表示函数调用是否成功。errors包则提供了创建和处理错误的函数和类型。
在这个例子中,我们使用了github.com/pkg/errors包,它是一个扩展了标准库errors包的第三方库。它提供了一些额外的功能,如堆栈跟踪和错误包装。
函数divFunc接收两个整数作为参数,如果第二个整数为0,则返回一个error类型的值,表示除数不能为0;否则返回计算结果和nil。在这个例子中,我们将0作为除数,意味着我们会遇到一个错误。我们使用errors包的New函数创建了一个新的错误,然后返回它作为第二个返回值。
在主函数中,我们调用了divFunc函数,并将其结果赋给ret和err两个变量。如果err不为nil,则表示函数调用失败,我们使用fmt.Printf函数打印错误信息。在这个例子中,我们使用了%+v格式化字符串,它可以打印完整的错误信息,包括堆栈跟踪和错误消息。如果err为nil,则表示函数调用成功,我们可以使用ret变量的值进行后续操作。
package mainimport ("fmt"perrors "github.com/pkg/errors"
)/*
go的error和其他
*/
func divFunc(a, b int) (int, error) {if b == 0 {return 0, perrors.New("b can't be zero")}return a / b, nil
}
func main() {var a, b = 1, 0ret, err := divFunc(a, b)if err != nil {fmt.Printf("ret is %d,err is %+v\n", ret, err)}
}
这段代码定义了一个Student结构体和一个NewStudent函数。NewStudent函数会返回一个新的Student实例,如果设置名字失败则返回一个错误。代码中使用了github.com/pkg/errors包的Wrap函数,用于将错误包装成更详细的错误信息。
Student结构体有两个字段,Name和Age。SetName方法用于设置Name字段的值,并且如果Name字段或传入的name参数为空,则返回一个自定义的DBError类型的错误,ErrNameEmpty。
NewStudent函数会返回一个新的Student实例。在这个函数中,我们首先创建一个18岁的Student实例,然后使用SetName方法设置名字。如果SetName方法返回了一个错误,则使用Wrap函数将该错误包装起来,并添加一个更详细的错误信息,最后将错误返回。如果SetName方法没有返回错误,则返回创建的Student实例和nil。
在主函数中,我们调用NewStudent函数,并检查返回的错误。我们使用errors.As函数尝试将该错误转换为DBError类型的指针。如果转换成功,则打印"match"。
总的来说,这个例子展示了如何自定义错误类型,并使用errors包的Wrap函数将错误包装成更详细的错误信息。我们还演示了如何使用errors.As函数将错误转换为自定义错误类型的指针,以便在处理错误时可以根据错误类型进行特定的处理。
package mainimport ("fmt""github.com/pkg/errors"
)type DBError struct {msg string
}func (d *DBError) Error() string {return d.msg
}var ErrNameEmpty = &DBError{"name can't be empty"}func (s *Student) SetName(name string) error {if name == "" || s.Name == "" {return ErrNameEmpty}s.Name = namereturn nil
}type Student struct {Name stringAge int
}func NewStudent() (*Student, error) {stu := &Student{Age: 18,}err := stu.SetName("")if err != nil {return stu, errors.Wrap(err, "set name faild")}return stu, nil
}
func main() {_, e := NewStudent()//%+v 显示错误信息和堆栈信息,比如文件名 行号等var perr *DBErrorif errors.As(e, &perr) {fmt.Println("match")}
}
Wrap() 函数在已有错误基础上同时附加堆栈信息和新提示信息
WithMessage() 函数在已有错误基础上附加新提示信息
WithStack() 函数在已有错误基础上附加堆栈信息。在实际中可根据情况选择使用
2xx 3xx 4xx 5xx
错误码, 错误码应该可以转换成http状态码? 但是可维护性很差,自动完成?
错误码设计的原则 ,http响应的策略:
{"code": 101010,"msg": "internal error","status": 1,0,"data":{}
}
如果我现在想要引入一个第三方监控系统,protmetheus, 无法完成主动监控, 都会去适配主流的http状态
2. 内部错误我们可以转换成对应的http状态码
twitter
http code
{"code": 101010,"message": "internal error","reference":"http://"
}
code设计思路:
错误码设计的重要性
net/http 状态码不够
采用数据 100111
10: 服务 api服务,mxshop后台管理系统服务, 最多可以有100个服务
01: 代表服务下的某个模块 商品管理 商品分类管理 每个服务可以有100个模块
11: 商品查询不到,商品已经下架 每个模块下支持100个错误码
错误码映射到http状态码
404 400
200 执行成功 get
201 post数据 新增成功
401: 认证失败
403: 权限不足
404: 资源找不到
400: 参数错误
500: 服务器错误
有一些公共的和业务无关的状态码我们就可以先行创建好
10 00 通用错误
10 01 01 02 通用-数据库错误
10 02 通用-认证失败
10 03 通用- 编解码错误
11 01 00商品模块 - 商品不存在,商品新建失败
错误统一采用大写开头
msg应该尽量简介 不要暴露过多的信息
此注释是为了后续AST代码生成做的一套规范 必须这种格式写
package codeconst (// ErrShopCartItemNotFound - 404: ShopCart item not found.ErrShopCartItemNotFound int = iota + 100701// ErrSubmitOrder - 400: Submit order error.ErrSubmitOrder// ErrOrderNotFound - 404: No Goods selected.ErrNoGoodsSelect
)
error包:链接:https://pan.baidu.com/s/1-bhHyFedg8LLEOo6ItVBtw?pwd=1234
提取码:1234
这个包已经达到了生产级别的功能了,所以直接到项目中使用就行,一般放到pkg目录下
code.go:实现code注册到errors包中
package codeimport ("github.com/novalagung/gubrak""mxshop/pkg/errors""net/http"
)type ErrCode struct {//错误码C int//http的状态码HTTP int//扩展字段Ext string//引用文档Ref string
}func (e ErrCode) HTTPStatus() int {return e.HTTP
}func (e ErrCode) String() string {return e.Ext
}func (e ErrCode) Reference() string {return e.Ref
}func (e ErrCode) Code() int {if e.C == 0 {return http.StatusInternalServerError}return e.C
}func register(code int, httpStatus int, message string, refs ...string) {found, _ := gubrak.Includes([]int{200, 400, 401, 403, 404, 500}, httpStatus)if !found {panic("http code not in `200,400,401,403,404,500`")}var ref stringif len(refs) > 0 {ref = refs[0]}coder := ErrCode{C: code,HTTP: httpStatus,Ext: message,Ref: ref,}errors.MustRegister(coder)
}var _ errors.Coder = (*ErrCode)(nil)
新建code_generated.go
:
这段代码应该由AST自动生成 并不是自己去维护它
package codefunc init() {register(ErrShopCartItemNotFound, 404, "ShopCart item not found.")
}
目录:
生成grpc所需文件:protoc -I. --go_out=. --go-grpc_out=. hello.proto
proto文件:
syntax = "proto3";//go:generate protoc -I. --go_out=. --go-grpc_out=. hello.protopackage template;option go_package="./;v1";// The greeting service definition.
service Greeter {// Sends a greetingrpc SayHello (HelloRequest) returns (HelloReply) {}
}// The request message containing the user's name.
message HelloRequest {string name = 1;
}// The response message containing the greetings
message HelloReply {string message = 1;
}
client.go:
package mainimport ("context""flag""fmt""log""mxshop/pkg/errors""time""google.golang.org/grpc""google.golang.org/grpc/credentials/insecure"_ "mxshop/app/pkg/code"pb "mxshop/cmd/order/rpc"
)const (defaultName = "world"
)var (addr = flag.String("addr", "localhost:50051", "the address to connect to")name = flag.String("name", defaultName, "Name to greet")
)func main() {flag.Parse()// Set up a connection to the server.conn, err := grpc.Dial(*addr, grpc.WithTransportCredentials(insecure.NewCredentials()))if err != nil {log.Fatalf("did not connect: %v", err)}defer conn.Close()c := pb.NewGreeterClient(conn)// Contact the server and print out its response.ctx, cancel := context.WithTimeout(context.Background(), time.Second)defer cancel()r, err := c.SayHello(ctx, &pb.HelloRequest{Name: *name})if err != nil {s := errors.FromGrpcError(err)coder := errors.ParseCoder(s)fmt.Println(coder.Code())//log.Fatalf("could not greet: %v", err)}log.Printf("Greeting: %s", r.GetMessage())
}
server.go:
package mainimport ("context""flag""fmt""log""mxshop/app/pkg/code""mxshop/pkg/errors""net""google.golang.org/grpc"pb "mxshop/cmd/order/rpc"
)var (port = flag.Int("port", 50051, "The server port")
)// server is used to implement helloworld.GreeterServer.
type server struct {pb.UnimplementedGreeterServer
}// SayHello implements helloworld.GreeterServer
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {log.Printf("Received: %v", in.GetName())e := errors.WithCode(code.ErrUserNotFound, "user not found")return nil, errors.ToGrpcError(e)//return nil, status.Error(codes.NotFound, "user not found")//return &pb.HelloReply{Message: "Hello " + in.GetName()}, nil
}func main() {flag.Parse()lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))if err != nil {log.Fatalf("failed to listen: %v", err)}s := grpc.NewServer()pb.RegisterGreeterServer(s, &server{})log.Printf("server listening at %v", lis.Addr())if err := s.Serve(lis); err != nil {log.Fatalf("failed to serve: %v", err)}
}
把code
注册到error包
中 再进行反解就可以拿到error
打印个code:
在Go语言项目中,使用AST(抽象语法树)生成代码和文档的好处有以下几点:
自动生成文档:通过解析Go语言源代码生成AST,可以自动生成函数、变量、结构体等API的文档,减少手动编写文档的工作量,提高文档的准确性和一致性。
代码重构:使用AST可以快速地解析和遍历源代码树,可以进行代码重构和自动生成代码等操作,大大提高了代码的可维护性和可扩展性。
代码检查:AST可以进行代码静态检查,例如检查代码中的语法错误、类型错误和潜在的错误,提高代码的质量和稳定性。
代码生成:通过AST可以生成代码,例如生成序列化和反序列化代码、协议处理代码等,可以提高开发效率,减少手写代码的错误和繁琐。
综上所述,使用AST生成代码和文档可以提高开发效率、减少错误和提高代码质量,是Go语言项目开发中非常有用的工具。
这段代码使用了Go语言的代码生成工具go:generate .
,并且使用了stringer
工具对ErrCode类型进行了枚举值的字符串转换。
具体来说,运行go generate
命令时,会自动执行stringer命令,并在同一目录下生成一个名为errcode_string.go
的文件,其中包含了ErrCode类型的String()方法的实现,用于将枚举值转换成字符串。
例如,code.ERR_CODE_INVALID_PARAMS
将返回字符串 参数错误 。
enum.go:
//go:generate stringer -type ErrCode -linecomment
package codetype ErrCode int64 //错误码// 错误码
const (ERR_CODE_OK ErrCode = 0 //okERR_CODE_INVALID_PARAMS ErrCode = 1 //参数错误ERR_CODE_TIMEOUT ErrCode = 2 //超时
)
main.go:
//go:generate stringer -type ErrCode -linecomment
package mainimport ("NewGo/generate/code""fmt"
)func main() {fmt.Println(code.ERR_CODE_INVALID_PARAMS)
}
直接贴个别人的文章做参考吧
关于AST的文章比较少,这篇算是好的了:https://zhuanlan.zhihu.com/p/380421057
AST语法树的基本元素可以根据上述文章和这个网站做解析:https://astexplorer.net/
学习好AST语法后进行变量名、注释的解析,并写入文件:
main.go:
package mainimport ("bytes""fmt""go/ast""go/parser""go/token""os""regexp""strings"
)var buf bytes.Bufferfunc main() {//设计code的文件fileName := "E:/zhuomian/project/v2/mxshop/app/pkg/code/user.go"//这个不需要过度的关心,得看源码,第3个选项是可以传代码 不用传文件,第4个是解析模式f, err := parser.ParseFile(token.NewFileSet(), fileName, nil, parser.ParseComments) if err != nil {panic(err)}fmt.Fprintf(&buf, "// Code generated by \"codegen %s\";DO NOT EDIT.\n", strings.Join(os.Args[1:], " "))fmt.Fprintf(&buf, "\n")fmt.Fprintf(&buf, "package %s", "code")fmt.Fprintf(&buf, "\n")fmt.Fprintf(&buf, "func init() {\n")//断言成GenDeclgenDecl(f.Decls[0].(*ast.GenDecl))fmt.Fprintf(&buf, "}\n")src := []byte(buf.String())err = os.WriteFile("E:/zhuomian/project/v2/mxshop/app/pkg/code/code_generated.go", src, 0o600)if err != nil {panic(err)}
}func genDecl(decl *ast.GenDecl) {for _, s := range decl.Specs {//断言成ValueSpec继续解析v := s.(*ast.ValueSpec)//考虑到有多个name,比如:ErrUserNotFound2,ErrUserNotFound3=1,2for _, name := range v.Names {var comment string//考虑到注释有/**/if v.Doc != nil && v.Doc.Text() != "" {comment = v.Doc.Text()//再考虑//} else if c := v.Comment; c != nil && len(c.List) == 1 {comment = c.Text()}httpCode, desc := ParseComment(name.Name, comment)fmt.Fprintf(&buf, "\tregister(%s, %s, \"%s\")\n", name.Name, httpCode, desc)fmt.Println(name.Name, httpCode, desc)}}
}func ParseComment(varName, comment string) (httpCode, desc string) {reg := regexp.MustCompile(`\w\s*-\s*(\d{3})\s*:\s*([A-Z].*)\s*\.\n*`)if !reg.MatchString(comment) {return "500", "Internal server error"}groups := reg.FindStringSubmatch(comment)//隐含有3块分组if len(groups) != 3 {return "500", "Internal server error"}return groups[1], groups[2]
}
自用的ast工具源码:链接:https://pan.baidu.com/s/1R-GsTEiql68syCf6umzw6w?pwd=1234
提取码:1234
直接go build
生成可执行文件扔到go/bin目录下
到code目录下执行go 你生成的可执行文件
它会找到所有含有//go:generate codeg
的文件(codeg是我命名的可执行文件)
然后生成源码
比如:
package code//go:generate codeg -type=intconst (// ErrUserNotFound - 404: User not found.ErrUserNotFound int = iota + 100401// ErrUserAlreadyExists - 400: User already exists.ErrUserAlreadyExists// ErrUserPasswordIncorrect - 400: User password incorrect.ErrUserPasswordIncorrect// ErrSmsSend - 400: Send sms error.ErrSmsSend// ErrCodeNotExist - 400: Sms code incorrect or expired.ErrCodeNotExist// ErrCodeInCorrect - 400: Sms code incorrect.ErrCodeInCorrect
)
到对应目录下执行go generate
会生成对应的源码
如果把//go:generate codeg -type=int
改为//go:generate codeg -type=int -doc -output ./error_code_generated.md
则会生成MD文档 如图: