安装依赖
1
|
go install github.com/zeromicro/go-zero/tools/goctl@latest
|
查看是否安装成功,如果未成功查看是否未将 $GOPATH/bin
加入PATH中,go install
默认会安装到GOPATH中
在1.3.3版本后,goctl可以执行命令自动安装protobuf依赖,但是默认安装在GOPATH中,因此一定要将 $GOPATH/bin
加入PATH中
安装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版本号要与模板对应的版本号一致