Contents

golang-go-zero-教程-Rpc

go-zero官网

go-zero详细文档

本系列为作者跟着Mikaelemmmm的b站教学视频学习时做的笔记

zero基于zrpc,zrpc基于grpc

rpc没有handler,直接就是logic

protobuf语法

protobuf语法

语法版本

1
syntax = "proto3";

proto包名

1
package pb;

生成的go文件目录

相对路径必须加./

1
option  go_package = "./pb";

传递参数

1
2
3
message GetUserInfoReq {
  int64 id = 1;
}

rpc服务调用

1
2
3
service usercenter {
  rpc GetUserInfo(GetUserInfoReq) returns (GetUserInfoResp);
}

goctl构建命令

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

目录结构

main文件

逻辑与api大致相同,只是起的服务不是http服务而是rpc服务

etc/config.yaml

rpc配置文件

internal/config/config.go

rpc配置文件对应的配置对象

internal/svc

他也是使用svcCtx来放置依赖

internal/logic

业务逻辑,rpc没有handler

internal/server

类似handler,但是用类似logic的方法封装为结构体

pb目录

.pb.go文件放序列化的结构体

_gprc.pb.go文件放grpc生成的方法

客户端目录

每定义一个service都会生成一个客户端目录目录名和service名一致,目录中只有一个.go文件文件名和service名一致,这个文件用于api或其他rpc服务调用本rpc的服务

postman调试grpc

在APIs里面导入.proto文件

然后在collections里面新建rpc collection 并新建grpc请求,选择你导入的API

Proto编写注意事项

尽量使用原生的proto数据类型,比如int64,string等。

时间戳直接使用int64,从数据库读出来的时间是time.Time结构体,可以相互转化

rpc或api int64–time.Unix(in.xxxtime)–>time.Time结构体x rpc或api int64<–x.Unix()–time.Time结构体x

尽量使用具体的类型,尽量不要去使用any类型

文件拆分:可以把message分类放文件夹里面,然后import。import文件之后需要使用包名.类型名来定义类型。拆分的proto文件需要手动使用protoc对每一个proto生成go文件,goctrl最后对有service的proto文件生成代码。

1
protoc -I ./ --go_out=paths=source_relative:. --go-grpc_out=paths=source_relative:. userModel.proto

另外相同结构尽量使用组合方式进行复用

main文件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
func main() {
	...
	s := zrpc.MustNewServer(c.RpcServerConf, func(grpcServer *grpc.Server) {
		pb.RegisterUsercenterServer(grpcServer, server.NewUsercenterServer(ctx))

		if c.Mode == service.DevMode || c.Mode == service.TestMode {
			reflection.Register(grpcServer)
		}
	})
  ...
}

grpcui需要执行reflection.Register(grpcServer),在yaml中配置服务模式Mode为dev或者test,才能使用

我推荐使用postman进行测试

1
2
3
...
Mode: dev
...

RPC使用Model

和api是一模一样的,直接把Model放到svcCtx,然后直接在logic里面使用

业务体量小的时候可以直接让api使用Model就行,当业务量起来了开发微服务就将Model的业务放到rpc上。想要严格只能由rpc调用Model,则直接将Model放到rpc的internal目录中,这样就只有rpc能够直接使用Model,之后再通过rpc 接口暴露使用Model的方法供api调用。

rpc拦截器

rpc没有middleware只有拦截器

分为客户端拦截器和服务端拦截器

服务端拦截器

直接在main中调用s.AddUnaryInterceptors方法注册拦截器

1
2
3
4
5
6
7
8
func main() {
	...
	s.AddUnaryInterceptors(func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
		
	})
  ...
	s.Start()
}

这个拦截器函数可以放到任意位置,只要格式是这样的就行

拦截器的逻辑:和api的middleware是一致的,都是洋葱模型,handle(ctx,req)和next(w,r)是一致的

  • 直接return则相当于直接丢弃
  • 调用前是处理前逻辑
  • 调用处理请求
  • 调用后是处理后逻辑
  • 最后需要return resp,err 来返回处理结果
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
func TestServerInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {

	fmt.Println("TestServerInterceptor ====> ")

	fmt.Printf("req ====> %+v \n", req)
	fmt.Printf("info ====> %+v \n", info)

	resp, err = handler(ctx, req)

	fmt.Printf("resp ====> %+v \n", resp)

	return resp, err
}

客户端拦截器

api中的拦截器

在svcCtx工厂函数中给zrpc.MustNewClient函数传递第二个参数,该参数为拦截器函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
func NewServiceContext(c config.Config) *ServiceContext {
	return &ServiceContext{
		...
		UserRpcClient: usercenter.NewUsercenter(zrpc.MustNewClient(c.UserRpcConf, zrpc.WithUnaryClientInterceptor(func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {

			//拦截前

			err := invoker(ctx, method, req, reply, cc, opts...)
			if err != nil {
				return err
			}

			//拦截后

			return nil
		}))),
	}
}

该函数也可以放到任意位置

invoker(ctx, method, req, reply, cc, opts…)和api的next以及rpc的handle是一致的

metadata传值

一般在拦截器里面实现,也可以在其他地方实现。这里展示拦截器实现过程

客户端拦截器添加metadata

1
2
3
//拦截前
md := metadata.New(map[string]string{"username": "zhangsan"})
ctx = metadata.NewOutgoingContext(ctx, md)

服务端logic读取metadata

1
2
3
4
if md, ok := metadata.FromIncomingContext(l.ctx); ok {
  tmp := md.Get("username")
  fmt.Printf("tmp: %+v\n", tmp)
}

metadata是map[string]string还是map[string][]string

MD的源码定义是map[string][]string,但是它的new工厂函数传入参数是map[string]string,但是它还有Get,Set方法,它们分别传入返回的值都是map[string][]strring。所以只是New的时候需要传入map[string]string,其他操作都是map[string][]string。

rpc启动源码解析

b站视频

 |