微服务框架 go-zero 快速实战| 青训营

169 阅读5分钟

微服务框架 go-zero 快速实战| 青训营

根据之前的学习,我们了解到了go-zero 是一个集成了各种工程实践的 web 和 rpc 框架,那么我们就可以来设计 web 部分的接口rpc 部分的接口

假如,我们有一个需求:

  • 例如有一个订单场景,我们需要查询某个租户的地址
  • 另外在租户系统这边,需要添加租户

我们知道,对于用户来说,访问的自然是 http 接口,那对于查询具体的租户信息,自然是内部微服务来进行处理

一般来说,HTTP 接口对外, RPC 接口对内

正如下面所示:

image.png

自定义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代码,当然也可以和之前讲的一样直接用编译器的插件一键生成,如下所示服务的目录结构:

image.png

自定义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 .

目录结构如下所示:

image.png

数据库

上面两个部分讲解了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 .

目录结构如下所示:

image.png

其中:

  • 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),
   }
}