go的web框架有很多,目前go的社区大家对于框架的态度也不尽相同,有些轻量级的框架,但是也就代表整合第三方中间件就需要自己根据客户端进行封装,比如gin+gorm,也有些功能完全但是被认为丢失了go本身轻量设计的初衷,
比如goframe,而同样的微服务有很多框架,国内比较出门的就是go-zero ,有专门的开发工具goctl,让开发者只需要关注业务代码即可完成微服务的上线。
介绍
go-zero 是一个集成了各种工程实践的 web 和 rpc 框架。通过弹性设计保障了大并发服务端的稳定性,经受了充分的实战检验。
go-zero 包含极简的 API 定义和生成工具 goctl,可以根据定义的 api 文件一键生成 Go, iOS, Android, Kotlin, Dart, TypeScript, JavaScript 代码,并可直接运行。
开发者只需要编写业务代码 就可以完成微服务的构建
红色代表需要开发者手写的部分!
所以重点是通过goctl 来根据编写的api文件和proto文件快速生成代码,让大部分时间只关心业务,缩短时间成本,这一点使用起来比goframe成熟一些(个人感觉),写下该笔记也是因为现在看文档的时候觉得go-zero 没有之前的文档那样对新手友好了
安装教程
安装官方脚手架 go 从ctroller
bash
#官方脚手架
go install github.com/zeromicro/go-zero/tools/goctl@latest
#protobuf 工具
goctl env check --install --verbose --force
#框架
go get -u github.com/zeromicro/go-zero@latest
验证版本
bash
goctl -version
快速入门
web模块
go
//生成 api web模块 目录greet
goctl api new greet
cd greet
go mod init
go mod tidy
go run greet.go -f etc/greet-api.yaml
此时访问localhost:8888就会得到一个null 但是控制器有对应的输出日志
根据api文件 生成web服务 官方的快速入门案列 zero-doc/doc/shorturl.md at main · zeromicro/zero-doc (github.com)
bash
#生成一个基本的api文件 api是go-zero的一个文件 用于goctl来快速的生成web代码
goctl api -o shorturl.api
替换内容为
apl
type (
expandReq {
shorten string `form:"shorten"`
}
expandResp {
url string `json:"url"`
}
)
type (
shortenReq {
url string `form:"url"`
}
shortenResp {
shorten string `json:"shorten"`
}
)
service shorturl-api {
@handler ShortenHandler
get /shorten(shortenReq) returns(shortenResp)
@handler ExpandHandler
get /expand(expandReq) returns(expandResp)
}
-
service shorturl-api {
这一行定义了 service 名字 -
@server
部分用来定义 server 端用到的属性 -
handler
定义了服务端 handler 名字 -
get /shorten(shortenReq) returns(shortenResp)
定义了 get 方法的路由、请求参数、返回参数等 -
type生成的交互结构体也会在对应文件夹
感觉和proto文件差不多 也是修改该文件快速生成代码
进行该目录 根据api文件 生成代码
goctl api go -api shorturl.api -dir .
后续如果数据结构以及更新接口也是编辑api文件,编辑生成的go文件会报错不应该生成源文件
bash
goctl api go -api order.api -dir .
并且不会覆盖已经写好的逻辑
shell
etc/web-api.yaml exists, ignored generation
internal/config/config.go exists, ignored generation
web.go exists, ignored generation
internal/svc/servicecontext.go exists, ignored generation
internal/handler/webhandler.go exists, ignored generation
internal/handler/orderhandler.go exists, ignored generation
internal/logic/weblogic.go exists, ignored generation
internal/logic/orderlogic.go exists, ignored generation
Done.
生成的web目录结构
.
├── api
│ ├── etc
│ │ └── shorturl-api.yaml // 配置文件
│ ├── internal
│ │ ├── config
│ │ │ └── config.go // 定义配置
│ │ ├── handler
│ │ │ ├── expandhandler.go // 实现 expandHandler
│ │ │ ├── routes.go // 定义路由处理
│ │ │ └── shortenhandler.go // 实现 shortenHandler
│ │ ├── logic
│ │ │ ├── expandlogic.go // 实现 ExpandLogic
│ │ │ └── shortenlogic.go // 实现 ShortenLogic
│ │ ├── svc
│ │ │ └── servicecontext.go // 定义 ServiceContext
│ │ └── types
│ │ └── types.go // 定义请求、返回结构体
│ ├── shorturl.api
│ └── shorturl.go // main 入口定义
├── go.mod
└── go.sum
-
type存放api生成的接口响应值和请求值
-
svc上下文变量 在rpc模块的时候需要经常用到
-
logic 是逻辑实现模块 类似mvc中接口的实现类
-
handler为路由注册,一个路由/XXX/XX 对应一个handler
-
任意打开生成的一个handler
和gin goframe一样也是对原生http请求进行封装 其中调用logic把开发者的逻辑和返回值写回http响应体
-
-
config 运行时候的上下文配置
运行
bash
go run shorturl.go -f etc/shorturl.yaml
启动类解析代码
go
#如果把这里的目录地址换成项目的引用地址
var configFile = flag.String("f", "shorturl/api/etc/shorturl.yaml", "the config file")
#作用是自定义指令 如果没有使用-f指定配置文件 则默认是第二个参数位置
func main() {
flag.Parse()
var c config.Config
conf.MustLoad(*configFile, &c)
server := rest.MustNewServer(c.RestConf)
//关闭服务
defer server.Stop()
ctx := svc.NewServiceContext(c)
handler.RegisterHandlers(server, ctx)
fmt.Printf("Starting server at %s:%d...\n", c.Host, c.Port)
server.Start()
}
就可以不用写-f参数 在idea运行
生成web模块的其他语言代码
goctl api java -api greet.api -dir greet
goctl api dart -api greet.api -dir greet
官网框架概述 | go-zero Documentation
rpc协议采用的是grpc 需要根据官网安装工具
rpc模块
新建一个目录
在 shorturl 目录下创建 rpc/transform 目录
bash
rpc/transform
在 rpc/transform 目录下编写 transform.proto 文件
可以通过命令生成 proto 文件模板
protobuf
goctl rpc -o transform.proto
修改后文件将内容替换如下:
protobuf
syntax = "proto3";
package transform;
option go_package = "./transform";
message expandReq{
string shorten = 1;
}
message expandResp{
string url = 1;
}
message shortenReq{
string url = 1;
}
message shortenResp{
string shorten = 1;
}
service transformer{
rpc expand(expandReq) returns(expandResp);
rpc shorten(shortenReq) returns(shortenResp);
}
用 goctl 生成 rpc 代码,在 rpc/transform 目录下执行命令
bash
goctl rpc protoc student.proto --go_out=. --go-grpc_out=. --zrpc_out=.
如果后续更新接口 响应体信息 生成代码单不覆盖已经写好的逻辑结构 支付该rpc 结构部分
protobuf
protoc --go_out=. --go-grpc_out=. order.proto
注意:不能在 GOPATH 目录下执行以上命令
文件结构如下:
rpc/transform
├── etc
│ └── transform.yaml // 配置文件
├── internal
│ ├── config
│ │ └── config.go // 配置定义
│ ├── logic
│ │ ├── expandlogic.go // expand 业务逻辑在这里实现
│ │ └── shortenlogic.go // shorten 业务逻辑在这里实现
│ ├── server
│ │ └── transformerserver.go // 调用入口, 不需要修改
│ └── svc
│ └── servicecontext.go // 定义 ServiceContext,传递依赖
├── transform
│ ├── transform.pb.go
│ └── transform_grpc.pb.go
├── transform.go // rpc 服务 main 函数
├── transform.proto
└── transformer
└── transformer.go // 提供了外部调用方法,无需修改
执行 go mod tidy 整理依赖
启动 etcd server (已经安装好)
bash
etcd
启动 rpc 服务直接可以运行,如下:
bash
go run transform.go -f etc/transform.yaml
Starting rpc server at 127.0.0.1:888...
查看服务是否注册,以下值为参考值,主要观察 etcd 有注册到 transform.rpc 的 key 和 8080 端口即可,各自机器的 ip 结果不一样。
bash
etcdctl get transform.rpc --prefix
#PS C:\Users\侯> etcdctl get transform.rpc --prefix
#transform.rpc/7587881007321565706
#169.254.67.206:888
目录文件解析
大致和api web项目的目录差不多 区别在于没有handler
和pro文件名一样的是goctl生成服务端代码
logic用来实现
如果后续需要抛出的接口很多 就续写proto文件即可
protoc --go_out=. --go-grpc_out=. student.proto
因为rpc注册到etcd的时,服务之间内部采用key通信
模拟真实场景
现在为了模拟一个真实场景 有以下学生商城的业务需求 用户向网关发送请求(采用普通web服务模拟)传递订单id ,网关转发该请求到order,order拿到该id以后向学生服务发起请求获取学生info的业务场景
用户->网关(web)->orderservice->studentservice 这样的微服务查询
如果是在java的那一套,编写配置类,编写接口,响应体,请求体需要的体量就大一些,go-zero使用api文件和grpc(proto)文件完成微服务的快速编写
目录结构
一个网关 一个学生服务 一个订单
编写student-service
我个人的习惯对于需求的链式调用是从底层写到高层
student.proto
proto
syntax = "proto3";
//指定生成的proto部分文件输出路径
option go_package="./student";
package student;
service StudentService {
rpc GetStudentInfo (StudentRequest) returns (StudentResponse);
}
message StudentRequest {
int32 student_id = 1;
}
message StudentResponse {
int32 student_id = 1;
string name = 2;
int32 age = 3;
}
定义了学生服务 ,该服务只有一个接口获取学生个人信息
BASH
goctl rpc protoc student.proto --go_out=. --go-grpc_out=. --zrpc_out=.
生成项目模板后 对grpc 只要熟悉的开发者就可以找到 在指定的输出路径有 生成的grpc服务和客户端
点击idea的实现提示就会跳转到 internal 目录下的server目录 ,而其中就包含 logic实列化来处理
go
func (s *StudentServiceServer) GetStudentInfo(ctx context.Context, in *student.StudentRequest) (*student.StudentResponse, error) {
l := logic.NewGetStudentInfoLogic(ctx, s.svcCtx)
return l.GetStudentInfo(in)
}
所以我们需要重写的就是生成在logic获取学生信息接口
模拟数据中,只要由id为1就可以成功调用
GO
func (l *GetStudentInfoLogic) GetStudentInfo(req *student.StudentRequest) (*student.StudentResponse, error) {
fmt.Println("学生服务被调用")
fmt.Printf("得到id%d\n", req.StudentId)
if req.StudentId == 1 {
return &student.StudentResponse{
StudentId: req.StudentId,
Name: "坏学生乔治",
Age: 20,
}, nil
}
return nil, nil
}
启动类的配置路径换成项目的引用路径
go
var configFile = flag.String("f", "quick_start/student_service/etc/student.yaml", "the config file")
点击启动
查看etcd keeper (etcd 的ui 插件) 可以看到已经成功注册了
提一下其中生成的studentservice 该目录包含了go-zero生成的接口交互服务文件,到时候服务之间交互就是引入的该文件
go
package studentservice
import (
"context"
"quickstart/quick_start/student_service/student"
"github.com/zeromicro/go-zero/zrpc"
"google.golang.org/grpc"
)
type (
StudentRequest = student.StudentRequest
StudentResponse = student.StudentResponse
StudentService interface {
GetStudentInfo(ctx context.Context, in *StudentRequest, opts ...grpc.CallOption) (*StudentResponse, error)
}
defaultStudentService struct {
cli zrpc.Client
}
)
func NewStudentService(cli zrpc.Client) StudentService {
return &defaultStudentService{
cli: cli,
}
}
func (m *defaultStudentService) GetStudentInfo(ctx context.Context, in *StudentRequest, opts ...grpc.CallOption) (*StudentResponse, error) {
client := student.NewStudentServiceClient(m.cli.Conn())
return client.GetStudentInfo(ctx, in, opts...)
}
编写order-service
最底部的学生服务被定义好后 ,那么就要编写订单服务,和学生服务不同的是,订单服务因为调用了学生服务,那么订单服务就要聚合学生服务
在order目录编写该文件
编写order.proto
proto
syntax = "proto3";
option go_package="./order";
package order;
service OrderService {
rpc GetOrderInfo (OrderRequest) returns (OrderResponse);
}
message OrderRequest {
int32 order_id = 1;
}
message OrderResponse {
int32 order_id = 1;
int32 student_id = 2;
string data = 3;
}
该文件也是定义了一个接口,以及其中响应体定义的data 用于装载student服务响应的json对象字符串
bash
goctl rpc protoc order.proto --go_out=. --go-grpc_out=. --zrpc_out=.
运行配置文件
自身也是微服务中的一环除去自身注册之外还需要定义一个需要引用到的学生rpc接口配置
etic/服务.yaml
yaml
Name: order-service
ListenOn: 127.0.0.1:9090
StudentRpcConf:
Etcd:
Hosts:
- localhost:2379
Key: student.rpc
Etcd:
Hosts:
- 127.0.0.1:2379
Key: order.rpc
上下文配置文件
因为需要使用到其他模块,go-zero服务之前的交互是通过封装的 zrpc等文件 所以需要引入上下文
internal中的config.go文件
GO
package config
import "github.com/zeromicro/go-zero/zrpc"
// Config is the configuration structure
type Config struct {
zrpc.RpcServerConf
StudentRpcConf zrpc.RpcClientConf
}
上下文件之中进行注册
svc目录下的上下文文件
go
package svc
import (
"github.com/zeromicro/go-zero/zrpc"
"quickstart/quick_start/order_service/internal/config"
//引入的就是student中 goctl生成的交互客户端文件
"quickstart/quick_start/student_service/studentservice"
)
type ServiceContext struct {
Config config.Config
StudentRpc studentservice.StudentService
}
func NewServiceContext(c config.Config) *ServiceContext {
return &ServiceContext{
Config: c,
//从配置文件服务端
StudentRpc: studentservice.NewStudentService(zrpc.MustNewClient(c.StudentRpcConf)),
}
}
好了那么就可以进行服务的逻辑实现 对应的逻辑实现
go
func (l *GetOrderInfoLogic) GetOrderInfo(req *order.OrderRequest) (*order.OrderResponse, error) {
fmt.Println("订单服务服务被调用")
fmt.Println("订单ID:", req.OrderId)
if req.OrderId == 1 {
// TODO: 模拟联查除订单id查询除学生id
studentResp, err := l.svcCtx.StudentRpc.GetStudentInfo(l.ctx, &student.StudentRequest{StudentId: 1})
if err != nil {
return nil, err
}
fmt.Println("学生信息:", studentResp)
toString, _ := jsonx.MarshalToString(studentResp)
return &order.OrderResponse{
OrderId: req.OrderId,
StudentId: studentResp.StudentId,
Data: toString,
}, nil
}
return nil, nil
}
到此位置 俩个微服务接口也就写完了,只需要编写网关 ,用户游览器给网关这个web服务发起请求,然后传递id 1就可以完成交互
和上面的一样修改启动类配置为引用地址启动
成功注册到etcd
web网关
编写web.api文件
api
syntax = "v1"
type Request {
Name string `path:"name,options=you|me"`
}
type OderRequest {
ID int `path:"id"`
}
type Response {
Message string `json:"message"`
}
type OrderResponse {
Message string `json:"message"`
Data string `json:"data"`
Code int `json:"code"`
}
service web-api {
@handler WebHandler
get /from/:name (Request) returns (Response)
@handler OrderHandler
get /order/:id (OderRequest) returns (OrderResponse)
}
有俩个接口 其中一个没有用 文件是从官网案列粘贴的模板 所以不管
web网关负责的是和直接用户交互,所以到时候其他服务获取的数据就装在data字段即可
bash
goctl api go -api web.api -dir .
生成代码后 ,同理开发者只要写逻辑相关代码即可
配置文件
定义调用服务的相关配置
YAML
Name: web-api
Host: 0.0.0.0
Port: 8888
Mode: dev
OrderRpcConf:
Etcd:
Hosts:
- localhost:2379
Key: order.rpc
上下文运行环境配置中引入
这里引入的字段名和配置文件中对应,zrpc源码根据这个进行赋值
GO
import (
"github.com/zeromicro/go-zero/rest"
"github.com/zeromicro/go-zero/zrpc"
)
type Config struct {
rest.RestConf
//定义引入的rpc服务
OrderRpcConf zrpc.RpcClientConf
}
rpc服务注册到上下文
go
"github.com/zeromicro/go-zero/zrpc"
"quickstart/quick_start/order_service/orderservice"
"quickstart/quick_start/web/internal/config"
)
type ServiceContext struct {
Config config.Config
OrderRpc orderservice.OrderService
}
func NewServiceContext(c config.Config) *ServiceContext {
return &ServiceContext{
Config: c,
OrderRpc: orderservice.NewOrderService(zrpc.MustNewClient(c.OrderRpcConf)),
}
}
logic完成调用逻辑
GO
func (l *OrderLogic) Order(req *types.OderRequest) (resp *types.OrderResponse, err error) {
// todo: add your logic here and delete this line
id := req.ID
fmt.Printf("Orderid:%d", id)
//新建一个rpc接口需要的参数
o := new(order.OrderRequest)
o.OrderId = int32(id)
if info, err := l.svcCtx.OrderRpc.GetOrderInfo(l.ctx, o); nil != err {
fmt.Printf("远程调用rpc失败")
} else {
resp := new(types.OrderResponse)
fmt.Println("订单信息:", info)
resp.Data = info.Data
resp.Code = 0
resp.Message = "服务调用成"
return resp, nil
}
//t := new(types.Response)
return
}
启动网关
api文件中定义的路由 ,是restful形式
get /order/:id (OderRequest) returns (OrderResponse)
生成的代码自然也是
GO
func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
server.AddRoutes(
[]rest.Route{
{
Method: http.MethodGet,
Path: "/from/:name",
Handler: WebHandler(serverCtx),
},
{
Method: http.MethodGet,
Path: "/order/:id",
Handler: OrderHandler(serverCtx),
},
},
)
}
游览器访问
成功输出
被调用服务日志也是成功输出
那么go-zero微服务的快速开发模式其实以及了解完全了,
剩下的就是对框架本身的一些规则感觉和gin这些go web框架差不多的部分了,中间件,路由绑定,参数和返回和其他数据库,微服务的中间件的整合了,由于篇幅问题 下次笔者在进行书写