go-zero入门

安装依赖

1
go install github.com/zeromicro/go-zero/tools/goctl@latest

查看是否安装成功,如果未成功查看是否未将 $GOPATH/bin 加入PATH中,go install 默认会安装到GOPATH中

1
goctl --version

在1.3.3版本后,goctl可以执行命令自动安装protobuf依赖,但是默认安装在GOPATH中,因此一定要将 $GOPATH/bin 加入PATH中

1
goctl env check -i -f

安装vscode插件 goctl,有写api 文件语法高亮等功能

官方文档:

1
https://go-zero.dev/docs/tutorials

生成

api文件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
syntax = "v1"

info (
	title:   "标题"
	desc:    "描述"
	author:  "作者"
	date:    "2023-12-08"
	version: "v1"
)

type UserInfoReq {
	// optional 可选参数,允许为零值
	Username string `json:"username,optional"`
	Password string `json:"password,optional"`
}

type UserInfoResp {
	Name string `json:"name"`
}

@server(
	// 文件分组
	group: user
	// 路由分组
	prefix: v1/user
	// 中间件
	middleware: UserAgentMiddleware
)

service user-api {
	@doc "获取用户信息"
	@handler userInfo
	post /user/info (UserInfoReq) returns (UserInfoResp)
}

type AdminInfoReq {
	Username string `json:"username,optional"`
	Password string `json:"password,optional"`
}

type AdminInfoResp {
	Name string `json:"name"`
}

@server(
	group: admin
)

// service 的名称必须相同
service user-api {
	@doc "获取Admin用户信息"
	@handler adminInfo
	post /admin/info (AdminInfoReq) returns (AdminInfoResp)
}

直接生成:

1
goctl api go -api *.api -dir ../ --style=goZero

含义是生成go的api,api文件地址是当前的 *.api,生成地址是上一级,命令风格是驼峰goZero,如果是 gozero,z小写则意味着生成的结构体都是纯小写命名

在 .api 文件中设置的中间件是局部中间件,如果要设置全局中间件则需要在main函数中通过 server.Use(middleware.NewMiddleware().Handle) 来设置,执行流程是先执行全局中间件然后再是局部中间件

proto文件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
syntax = "proto3";

option go_package = "./pb";

package pb;


//model
message UserInfoReq {
  int64  id = 1;
  string  Username=2;
	string Password=3;
}

message UserInfoResp {
  string  Name=1;
}

//service
service usercenter {
  rpc getUserInfo(UserInfoReq) returns(UserInfoResp);
}

生成代码:

1
goctl rpc protoc *.proto --go_out=../ --go-grpc_out=../ --zrpc_out=../ --style=goZero

可以通过grpcui 来独立调试grpc程序,vscode的postman不太好用且没有历史记录

proto文件设计原则:

  • 尽量使用时间戳格式,int64好传,而时间格式需要引用新包
  • 不用interface any类型,可读性不好
  • 复用之前的结构体,不要明明之前定义了一个结构体,新的结构体有很多字段都存在却复制之前的所有字段然后加,这样会导致proto文件很长,可读性差
  • 可以通过拆分文件缩小proto文件的大小,比如将一些大型的结构体放到另一个文件中,通过protoc来生成,然后再goctl生成包含service的proto文件,通过 import "test1.proto"; 的方式引用,然后在需要的地方写 pb.Inforeq=1; 来定义,pb是引用的protobuf文件中的 package定义的包名
1
protoc -I ./ --go_out=paths=source_relative:. --go-grpc_out=paths=source_relative:. test1.proto

model文件

直接输入命令:

1
goctl model mysql datasource -url="root:123456@tcp(127.0.0.1:3306)/dbname" -table="user"  -dir="./" --style=goZero

生成之前要保证此数据库可以连接且库中存在此表,goctl会连接此数据库并根据表字段生成代码,这段命令中有部分参数其实是可以写死的,因此可以写一个脚本

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/bin/bash

# 使用方法:
# ./model.sh 库名 表名
# 再将生成的文件剪切到对应服务的model目录里面,记得改package


#生成的表名
tables=$2
#表生成的genmodel目录
modeldir=./genModel

# 数据库配置
host=127.0.0.1
port=3306
dbname=looklook_$1
username=root
passwd=


echo "开始创建库:$dbname 的表:$2"
goctl model mysql datasource -url="${username}:${passwd}@tcp(${host}:${port})/${dbname}" -table="${tables}"  -dir="${modeldir}" --style=goZero

dockerFile文件

1
goctl docker -go user.go

基于user.go 生成dockerFile文件,user.go中有main函数

k8s的yaml文件

1
goctl kube deploy -name user-api -namespace zerodemo -image user-api:v1.0 -o user-api.yaml -port 1001

操作集

初始化

如果要操作DB或者要调用远端grpc函数或者增加中间件首先在 yaml文件中配置好,并需要在conf和svc中进行初始化

config文件夹 config.go:

1
2
3
4
5
6
7
type Config struct {
	rest.RestConf
	UserRpcConf zrpc.RpcClientConf
	DB          struct {
		DataSource string
	}
}

这个go文件和配置文件中的配置是一一对应的,程序启动时会将配置文件隐射到此对象中

svc文件夹serviceContext.go:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
type ServiceContext struct {
	Config              config.Config
	UserAgentMiddleware rest.Middleware
	UserModel           model.UserModel
	UserRpcClient       usercenter.Usercenter
}

func NewServiceContext(c config.Config) *ServiceContext {
	return &ServiceContext{
		Config:              c,
		UserAgentMiddleware: middleware.NewUserAgentMiddleware().Handle,
		UserModel:           model.NewUserModel(sqlx.NewMysql(c.DB.DataSource)),
		UserRpcClient:       usercenter.NewUsercenter(zrpc.MustNewClient(c.UserRpcConf)),
	}
}

main函数会调用new来初始化这些对象,随后在会话中通过接受者的svcCtx来调用这些对象方法

事务

1
2
3
	m.TransactCtx(ctx,func(ctx context.Context, s sqlx.Session) error {
		return nil
	})

开启事务,在func中写事务代码,go-zero会根据返回的错误是否为空来进行判断是否需要回滚,不需要自己回滚,只需要在需要回滚的时候返回err即可

不过很多时候是在logic中操作事务,此时就可以将这个方法暴露给logic

1
2
3
4
5
func (m *defaultUserModel) TransCtx(ctx context.Context, fn func(context.Context, sqlx.Session) error) error{
	return m.TransactCtx(ctx,func(ctx context.Context, s sqlx.Session) error {
		return fn(ctx,s)
	})
}

如果要操作事务,一定要用一个会话来进行操作,即连接是 sqlx.Session 而不是 sqlx.SqlConn

日志

在开发环境中,日志使用go-zero的logx包打印信息,如果使用Path来保存日志,需要关注的就是slow.log和error.log,slow关注慢查询,error是灾难型错误

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# Log对象通常是不用自己定义的,在框架内部有设置默认值,直接logx.Info或Error打印就行,如果线上需要不打印info等级就需要配置这个日志等级
Log:
  # 服务名称
  ServiceName: user-api
  # 输入位置
  Model: console
  # 日志等级
  Level: error
  # 编码格式,通常线上为json,而开发环境建议使用plain,因为plain打印更好阅读,比如打印堆栈信息时,json没有换行,而plain会进行换行,更适合阅读,但是线上需要使用json来节省内容空间和统一格式
  Encoding: plain

携程

如果使用 go func(){} 来起一个携程,有时会忘记对它进行recover操作,因此可以使用 threading.GoSafe 的方式来安全的启动一个携程,它底层本身也只是对携程进行一个封装,附带了一个当前函数区间的recover,避免panic没有被捕获,也可以自己对goroutinue进行封装

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
func GoSafe(fn func()) {
	go RunSafe(fn)
}

func RunSafe(fn func()) {
	defer Recovers()

	fn()
}

func Recovers(cleanups ...func()) {
	// 资源回收函数
	for _, cleanup := range cleanups {
		cleanup()
	}

	if p := recover(); p != nil {
		log.Println(p)
	}
}

连接rpc

通常上层连接底层需要先在配置文件中配置,并在config对象中定义和后续初始化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
UserRpcConf:
  # 用于直连方式,与其他方式冲突,当和target同时存在时,会导致k8s负载不均衡
  # 三种连接方式负载均衡协议都走的是p2c
  Endpoints: 
    - 127.0.0.1:8080
  # etcd 方式
  Etcd:
    Hosts:
    - 127.0.0.1:2379
    Key: user.rpc
  # k8s方式
  Target: k8s://godemo/svc:9090 # svc一定要是k8s rpc服务器 yaml文件的serviceName
1
2
3
4
5
6
7
8
type Config struct {
	rest.RestConf
	UserRpcConf zrpc.RpcClientConf
	DB          struct {
		DataSource string
	}
	Cache cache.CacheConf
}
1
2
3
4
5
6
7
8
func NewServiceContext(c config.Config) *ServiceContext {
	return &ServiceContext{
		Config:              c,
		UserAgentMiddleware: middleware.NewUserAgentMiddleware().Handle,
		UserModel:           model.NewUserModel(sqlx.NewMysql(c.DB.DataSource)),
		UserRpcClient:       usercenter.NewUsercenter(zrpc.MustNewClient(c.UserRpcConf)),
	}
}

rpc拦截器

它的作用和中间件比较类似,所有的请求在进入内部方法之前都会先经过拦截器,不过没有next之类的方法

rpc通过在main函数中使用 s.AddUnaryInterceptors() 来进行拦截

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
	s.AddUnaryInterceptors(func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp any, err error) {
		// 请求前
		fmt.Println("start inter")

		// 处理请求
		resp, err = handler(ctx, req)

		// 请求后
		fmt.Println("end inter")
		return resp, err
	})

如果想在rpc客户端(api端)进行拦截则需要在 NewServiceContext() 函数中初始化它

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
func NewServiceContext(c config.Config) *ServiceContext {
	return &ServiceContext{
		Config:              c,
		UserAgentMiddleware: middleware.NewUserAgentMiddleware().Handle,
		UserModel:           model.NewUserModel(sqlx.NewMysql(c.DB.DataSource)),
		UserRpcClient: usercenter.NewUsercenter(zrpc.MustNewClient(c.UserRpcConf, zrpc.WithUnaryClientInterceptor(func(ctx context.Context, method string, req, reply any, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
			// 拦截函数 (要注意的是这个拦截是在调用rpc服务时触发的,与当前层的业务代码是没关联的,所以它不一定会触发在本层业务代码之前,那个属于中间件的范畴)

			md := metadata.New(map[string]string{"key": "value"})
			// 创建一个附加了传出 md 的新上下文,可供外部的 gRPC 客户端、服务端使用;NewIncomingContext仅供自身的 gRPC 服务端内部使用
			ctx = metadata.NewOutgoingContext(ctx, md)

			// 请求rpc
			err := invoker(ctx, method, req, reply, cc, opts...)

			// 请求后函数

			return err
		}))),
	}
}

可以通过metadata来存储有md的上下文,然后在底层获取使用(类似于http的header)

1
2
3
4
5
6
7
	if md, ok := metadata.FromOutgoingContext(l.ctx); ok {
	// tmp是一个字符串切片,主要是考虑到兼容性,在使用Pairs的方式来创建md时可能其值是一个切片字符串
		tmp := md.Get("key")
		if len(tmp) > 0 {
			fmt.Println(tmp)
		}
	}

传参校验

go-zero自带的校验能力较弱,推荐使用第三方校验库,如 https://github.com/go-playground/validator

如果使用第三方库校验,就需要在 .api 文件中添加它的tag校验语句

1
2
3
4
type UserInfoReq {
	Username string `json:"username,optional" validate:"gte=0,lte=130"`
	Password string `json:"password,optional"`
}

这样在生成代码时,type文件夹下的req和resp结构体也会附带validate tag,千万不要直接修改type文件夹下的内容,第一行就提示了 // Code generated by goctl. DO NOT EDIT. ,即修改了Type下一次生成又会覆盖它

除此之外,还需要在handler中增加它的校验函数

1
2
3
4
5
6
7
	user := &User{
		FirstName:      "Badger",
		LastName:       "Smith",
	}

	err := validate.Struct(user)
	if err != nil {

生成模板

如果go-zero默认的模板不适合自己的项目,可以修改模板文件,让其匹配自身的项目

通过 goctl template init 会在家目录下的 .goctl 下生成对应版本的模板文件,之后使用goctl 生成代码时就会使用此目录的模板,就可以通过修改家目录下的模板文件修改生成的代码内容,如果不init则是使用默认模板

不过这种方法只适合个人开发,如果是团队开发还是建议提交到git,让所有人都能通过这个模板生成一样的架构文件,避免混乱。成员可以clone下模板项目,然后在生成代码时通过 --home=模板文件夹地址 来指定使用哪个模板生成go-zero代码

修改模板文件时要注意的是,每个文件可以使用的变量都是不同的,我们只能修改模板文件,源码文件传递哪些变量是设置好了的。因此加方法一定要选择有对应变量的模板文件。

需要注意版本兼容问题,即goctl版本号要与模板对应的版本号一致

Licensed under CC BY-NC-SA 4.0