微服务框架 go-zero 快速实战| 青训营
根据之前的学习,我们了解到了go-zero 是一个集成了各种工程实践的 web 和 rpc 框架,那么我们就可以来设计 web 部分的接口和 rpc 部分的接口。
假如,我们有一个需求:
- 例如有一个订单场景,我们需要查询某个租户的地址
- 另外在租户系统这边,需要添加租户
我们知道,对于用户来说,访问的自然是 http 接口,那对于查询具体的租户信息,自然是内部微服务来进行处理
一般来说,HTTP 接口对外, RPC 接口对内。
正如下面所示:
自定义rpc接口
proto语法
我们已经知道了RPC使用protobuf(全称protocol buffers)作为其接口定义语言,通过protoc可以编译生成指定编程语言版本的代码,那么一个最简单的proto文件包括哪些内容呢?
以下就是从go-zero官网中截取的一个最简单基础的proto文件的实例:
// 声明 proto 语法版本,固定值
syntax = "proto3";
// proto 包名
package greet;
// 生成 golang 代码后的包名
option go_package = "example/proto/greet";
// 定义请求体
message SayHelloReq {}
// 定义响应体
message SayHelloResp {}
// 定义 Greet 服务
service Greet {
// 定义一个 SayHello 一元 rpc 方法,请求体和响应体必填。
rpc SayHello(SayHelloReq) returns (SayHelloResp);
}
除了最开始的固定设置的版本、包名、生成代码的包名,我们还需要根据自己的需求为每个服务设置请求体、响应体以及服务定义,服务定义体内部为它的rpc方法。
同时,如果先引入已有的proto文件,可以用import导入:
import "base/base.proto";
但是, goctl 根据 proto 生成 gRPC 代码时:
- service 中的 Message(入参&出参) 必须要在 main proto 文件中,不支持引入的文件
- 引入的 Message 只能嵌套在 main proto 中的 Message 中使用
- goctl 生成 gRPC 代码时,不会生成被引入的 proto 文件的 Go 代码,需要自行预先生成
我们有时候还会看到一个消息格式中像下面一样:
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
}
其中每个字段的第一个部分是字段的类型,包括但不限于整型、字符串、枚举型。
第二个部分是该字段的名称,而第三个部分不是指该字段的默认初始值,而是分配标识号,在消息定义中,每个字段都有唯一的一个数字标识符。这些标识符是用来在消息的二进制格式中识别各个字段的,一旦开始使用就不能够再改变。注:[1,15]之内的标识号在编码的时候会占用一个字节。[16,2047]之内的标识号则占用2个字节。所以应该为那些频繁出现的消息元素保留 [1,15]之内的标识号。切记:要为将来有可能添加的、频繁出现的标识号预留一些标识号。
生成rpc代码
根据前面的需求,我们可以编写如下的proto文件:
syntax = "proto3";
package tenant;
option go_package = "./tenant";
message TidReq {
string name = 1;
}
message TenantRsp {
// 租户id
int32 id = 1;
// 租户名称
string name = 2;
// 用户地址
string addr = 3;
}
message addTenantReq {
// 租户名称
string name = 1;
// 用户地址
string addr = 2;
}
message addTenantRsp {
// 租户 id
string id = 1;
}
service Tenant {
rpc getTenant(TidReq) returns(TenantRsp);
rpc addTenant(addTenantReq) returns(addTenantRsp);
}
然后使用gotcl自动生成golang代码,当然也可以和之前讲的一样直接用编译器的插件一键生成,如下所示服务的目录结构:
自定义api接口
我们已经完成了对内的rpc服务了,那么下面需要完成对外的api服务了。
首先编写api文件:
syntax = "v1"
type OrderReq {
Name string `path:"name"`
}
type OrderReply {
Id int32 `json:"id"`
Name string `json:"name"`
Addr string `json:"addr"`
}
service order {
@handler getOrder
get /api/order/get/:name (OrderReq) returns (OrderReply)
}
然后也可以使用编译器的插件一键生成,或者使用goctl命令生成:
goctl api go -api order.api -dir .
目录结构如下所示:
数据库
上面两个部分讲解了api服务和rpc服务的搭建,现在的服务都和数据库有关联,下面我们就需要在代码中创建数据库的连接,以及相应数据的增删改查等操作方法。
首先,我们先在本地数据库中创建使用的数据库名称为demo_go_zero。
然后新建一张表:
CREATE TABLE `tenant_info`
(
`id` int NOT NULL COMMENT 'tenant id',
`name` varchar(255) NOT NULL COMMENT 'tenant name',
`addr` varchar(255) NOT NULL COMMENT 'tenant addr',
PRIMARY KEY(`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
将该sql命令保存到/demo_go_rpc/tenant/rpc/model路径下,然后使用goctl根据自动生成golang代码的数据模型文件,当然编译器插件也可以一键生成:
goctl model mysql ddl -src tenant.sql -dir .
目录结构如下所示:
其中:
- tenant.sql: 这是输入的源 DDL 文件,其中包含了 MySQL 数据库的表结构定义和约束;
- tenantmodel.go:是根据给定的 MySQL DDL 文件生成的数据模型文件。它是由 Goctl 工具自动生成的,用于在应用程序中进行数据操作;
- tenantmodel.go:该文件是用于添加自定义方法;
- vars.go:这是生成的验证器文件,它包含了对表中数据进行验证的方法,用于在应用程序中进行数据校验。
然后先向表中插入一条测试数据,以便后面的测试:
insert into tenant_info values(1, "哪都通快递", "纳森岛501h");
最后,rpc要对数据库进行查询,需要找到数据库的DSN以及完善服务中对应结构体字段:
\demo_go_zero\tenant\rpc\etc\tenant.yaml:
Name: tenant.rpc
ListenOn: 0.0.0.0:8080
Etcd:
Hosts:
- 127.0.0.1:2379
Key: tenant.rpc
// 数据源和数据表的配置
DataSource: root:123456@tcp(localhost:3306)/demo_go_zero
Table: tenant_info
\demo_go_zero\tenant\rpc\internal\config\config.go:
type Config struct {
zrpc.RpcServerConf
DataSource string
Table string
}
\demo_go_zero\tenant\rpc\internal\svc\servicecontext.go:
type ServiceContext struct {
Config config.Config
Model model.TenantInfoModel
}
func NewServiceContext(c config.Config) *ServiceContext {
return &ServiceContext{
Config: c,
Model: model.NewTenantInfoModel(sqlx.NewMysql(c.DataSource), c.Cache),
}
}