本文是对 使用go-zero快速构建微服务的亲手实践


编写API Gateway代码


1
2
mkdir bookstore && cd bookstore
go mod init bookstore

mkdir api && goctl api -o api/bookstore.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
syntax = "v1"

info(
title: "爽哥使用go-zero"
desc: "爽哥用来上手go-zero"
author: "cuishuang"
email: "imcusg@gmail.com"
)

type (
addReq struct {
book string `form:"book"`
price int64 `form:"price"`
}

addResp struct {
ok bool `json:"ok"`
}
)

type (
checkReq struct {
book string `form:"book"`
}

checkResp struct {
found bool `json:"found"`
price int64 `json:"price"`
}
)


service bookstore-api {
@handler AddHandler
get /add (addReq) returns (addResp)

@handler CheckHandler
get /check (checkReq) returns (checkResp)
}

cd api && goctl api go -api bookstore.api -dir .

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
api
├── bookstore.api // api定义
├── bookstore.go // main入口定义
├── etc
│ └── bookstore-api.yaml // 配置文件
└── internal
├── config
│ └── config.go // 定义配置
├── handler
│ ├── addhandler.go // 实现addHandler
│ ├── checkhandler.go // 实现checkHandler
│ └── routes.go // 定义路由处理
├── logic
│ ├── addlogic.go // 实现AddLogic
│ └── checklogic.go // 实现CheckLogic
├── svc
│ └── servicecontext.go // 定义ServiceContext
└── types
└── types.go // 定义请求、返回结构体


go run bookstore.go -f etc/bookstore-api.yaml

启动API Gateway服务,默认侦听在8888端口

因为默认生成的api/etc/bookstore-api.yml为:

1
2
3
Name: bookstore-api
Host: 0.0.0.0
Port: 8888

按提示下载,再次运行:




1
2
3
{"@timestamp":"2023-02-16T16:31:09.658+08:00","caller":"stat/usage.go:61","content":"CPU: 0m, MEMORY: Alloc=2.5Mi, TotalAlloc=2.5Mi, Sys=14.5Mi, NumGC=0","level":"stat"}
{"@timestamp":"2023-02-16T16:31:09.662+08:00","caller":"load/sheddingstat.go:61","content":"(api) shedding_stat [1m], cpu: 0, total: 0, pass: 0, drop: 0","level":"stat"}
{"@timestamp":"2023-02-16T16:31:15.044+08:00","caller":"stat/metrics.go:210","content":"(bookstore-api) - qps: 0.0/s, drops: 0, avg time: 0.0ms, med: 0.0ms, 90th: 0.0ms, 99th: 0.0ms, 99.9th: 0.0ms","level":"stat"}


会定时(默认一分钟)输出cpu,内存等的统计信息,可以通过
logx.DisableStat()禁用 (可以做到自定义模板.tpl里)


返回的是null,并不是预期的{"found":false,"price":0}

这是因为:

resp是一个指针,这样直接return会是nil,需要如下显式声明

重启服务,再次发起请求,这样的response就符合预期了~


目前只返回了个空值,接下来会在rpc服务里实现业务逻辑

可以修改internal/svc/servicecontext.go来传递服务依赖(如果需要,比如Config,Auth,后续用到的RPC等)

实现逻辑可以修改internal/logic下的对应文件(如果接口较多,可以在.api里定义不同的group,使用goctl生成代码时,会自动在logic下根据group名称创建不同的文件夹)

可以通过goctl生成各种客户端语言的api调用代码(供客户端同学使用;支持多种语言)


编写RPC代码


编写add rpc服务


切到bookstore目录下

mkdir -p rpc/add && cd rpc/add

goctl rpc template -o add.proto

修改后文件内容如下:

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

package add;

option go_package = "./pb";

message addReq {
string book = 1;
int64 price = 2;
}

message addResp {
bool ok = 1;
}

service adder {
rpc add(addReq) returns(addResp);
}

goctl rpc protoc add.proto --go_out=./pb --go-grpc_out=./pb --zrpc_out=.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
rpc/add
├── add.go // rpc服务main函数
├── add.proto // rpc接口定义
├── adder
├── adder.go // 提供了外部调用方法,无需修改
├── adder_mock.go // mock方法,测试用
└── types.go // request/response结构体定义
├── etc
└── add.yaml // 配置文件
├── internal
├── config
└── config.go // 配置定义
├── logic
└── addlogic.go // add业务逻辑在这里实现
├── server
└── adderserver.go // 调用入口, 不需要修改
└── svc
└── servicecontext.go // 定义ServiceContext,传递依赖
└── pb
└── add.pb.go

go run add.go -f etc/add.yaml 可运行该服务



默认每隔一分钟输出cpu和内存信息
1
2
{"@timestamp":"2023-02-16T20:02:10.640+08:00","caller":"stat/usage.go:61","content":"CPU: 0m, MEMORY: Alloc=3.3Mi, TotalAlloc=6.2Mi, Sys=15.9Mi, NumGC=3","level":"stat"}
{"@timestamp":"2023-02-16T20:02:10.656+08:00","caller":"load/sheddingstat.go:61","content":"(rpc) shedding_stat [1m], cpu: 0, total: 0, pass: 0, drop: 0","level":"stat"}


编写check rpc服务

切到bookstore目录下

mkdir -p rpc/check && cd rpc/check

goctl rpc template -o check.proto

修改后文件内容如下:

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

package check;

option go_package = "./pb";

message checkReq {
string book = 1;
}

message checkResp {
bool found = 1;
int64 price = 2;
}

service checker {
rpc check(checkReq) returns(checkResp);
}

goctl rpc protoc check.proto --go_out=./pb --go-grpc_out=./pb --zrpc_out=.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
rpc/check
├── check.go // rpc服务main函数
├── check.proto // rpc接口定义
├── checker
├── checker.go // 提供了外部调用方法,无需修改
├── checker_mock.go // mock方法,测试用
└── types.go // request/response结构体定义
├── etc
└── check.yaml // 配置文件
├── internal
├── config
└── config.go // 配置定义
├── logic
└── checklogic.go // check业务逻辑在这里实现
├── server
└── checkerserver.go // 调用入口, 不需要修改
└── svc
└── servicecontext.go // 定义ServiceContext,传递依赖
└── pb
└── check.pb.go

go run check.go -f etc/check.yaml 可运行该服务

修改etc/check.yaml的端口为8081(因为8080已经被add服务使用了)


再回去修改API Gateway代码,调用add/check rpc服务


api/etc/bookstore-api.yaml,增加如下内容

1
2
3
4
5
6
7
8
9
10
Add:
Etcd:
Hosts:
- localhost:2379
Key: add.rpc
Check:
Etcd:
Hosts:
- localhost:2379
Key: check.rpc

通过etcd自动去发现可用的add和check服务


修改api/internal/config/config.go如下,增加add&check服务依赖


修改api/internal/svc/servicecontext.go,如下:

通过ServiceContext在不同业务逻辑之间传递依赖

(问:怎么解决依赖注入问题)


修改api/internal/logic/addlogic.go里的Add方法,如下:

通过调用adder的Add方法实现添加图书到bookstore系统


修改api/internal/logic/checklogic.go里的Check方法,如下:

通过调用checker的Check方法实现从bookstore系统中查询图书的价格


定义数据库表结构,并生成CRUD+cache代码


bookstore下创建rpc/model目录

mkdir -p rpc/model (不过一般习惯把这个model文件夹抽出来,和api,rpc在一层)

在rpc/model目录下编写创建book表的sql文件book.sql,如下:

1
2
3
4
5
6
CREATE TABLE `book`
(
`book` varchar(255) NOT NULL COMMENT 'book name',
`price` int NOT NULL COMMENT 'book price',
PRIMARY KEY(`book`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

进入mysql命令行,创建DB和table

1
2
create database gozero;
source book.sql;

在rpc/model目录下执行如下命令生成CRUD+cache代码,-c表示使用redis cache

goctl model mysql ddl -c -src book.sql -dir .


修改add rpc和check rpc,调用crud+cache代码


修改rpc/add/etc/add.yaml和rpc/check/etc/check.yaml,均增加如下内容:

1
2
3
4
5
DataSource: root:123456@@tcp(xxx.xxx.xx.xx:3306)/gozero
#DataSource: root:123456@@tcp(localhost:3306)/gozero?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai #可以这样指定一些其他信息
Table: book
Cache:
- Host: localhost:6379

可以使用多个redis作为cache,支持redis单点或者redis集群


修改rpc/add/internal/config.go和rpc/check/internal/config.go,如下:


修改rpc/add/internal/svc/servicecontext.go和rpc/check/internal/svc/servicecontext.go,如下:


修改rpc/add/internal/logic/addlogic.go,如下

修改rpc/check/internal/logic/checklogic.go,如下:


项目使用


需要先全部启动api服务所依赖的rpc服务。如果先启动api,则会报错:

error: context deadline exceeded, make sure rpc service "add.rpc" is already started

全部启动:

(后面可以 -f指定不同环境的xxx.yaml)

调用add api,新增图书

curl -i "http://localhost:8888/add?book=Bible&price=10"

此时看数据库,book表里新增了一行数据


调用check api,检查某本图书的价格

curl -i "http://localhost:8888/check?book=Bible"

重启check rpc,再次执行curl -i "http://localhost:8888/check?book=Bible"


完整项目代码