GoFrame & gRPC微服务学习笔记(一) 前言 由于项目要求,需要使用GoFrame框架和gRPC协议来构建微服务,所以需要学习一下GoFrame和gRPC。
在第一篇的学习中记录,主要是学习和如何使用GoFrame脚手架来快速建立一个微服务框架。
学习目标 了解GoFrame框架的基础架构 掌握gRPC的核心概念 搭建基础的微服务开发环境 Golang中的的微服务和Java中的微服务概念相差不大,微服务就是将一个大的系统拆分成多个小系统,每个小系统都可以独立运行,互不干扰。
同时微服务不直接对外提供服务,而是统一交给一个网关来对外提供服务,网关负责请求的转发和负载均衡。
网关作为一个Web服务,它不直接提供具体的业务功能,而是负责接收请求,转发到各微服务,最后拼接数据返回,以此来完成业务功能。
微服务之间通过grpc协议进行通讯,而网关对外提供http服务,内部则使用grpc协议与各个微服务进行通信。
而微服务的注册中心则使用etcd来实现。
学习内容 1.使用GoFrame脚手架快速建立一个微服务框架 微服务将单体服务拆开,代码也自然分离。它的代码仓库,有两种常见的管理方式:
Multirepo 多仓库模式,每个微服务都有独立的仓库。优点是每个仓库相对较小,易于管理。缺点是需要额外的工具,流程协调各个服务之间的依赖和版本。
Monorepo 单一仓库模式,所有微服务的代码都存放在一个仓库中。优点是可以统一管理版本和依赖,缺点是仓库可能会变得庞大,管理复杂度增加。
GoFrame脚手架支持这两种管理方式,并且提供了脚手架工具来快速创建微服务。
这里使用单一仓库模式。
gRPC是一个由 Google 开发的远程过程调用(RPC)框架,基于HTTP/2。它使用 Protobuf 作为默认的序列化格式。
在安装Go语言插件之前,需要先安装protobuf插件,需要注意的是,protobuf是是用于序列化结构化数据的与语言无关、与平台无关的可扩展机制。所以需要使用apt/dnf/brew等包管理工具来安装。当然也可以选择使用二进制进行安装。
我这里使用的是fedora41,使用dnf安装命令:
1 sudo dnf install protobuf
安装完毕后,可以输入protoc --version
来确认版本号。
1 2 $ protoc --version libprotoc 3.19.6
Go语言通过gRPC-go插件提供gRPC功能。执行命令安装插件:
1 2 go install google.golang.org/protobuf/cmd/protoc-gen-go@latest go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
在上述两者安装完毕后,则初步开发环境已经搭建完成。随机开始建立项目。
由于是一个微服务项目,我们建立总目录时不能使用传统的web结构,而是需要加一个-m
参数,表示使用Monorepo模式。
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 $ gf init --help USAGE gf init ARGUMENT [OPTION] ARGUMENT NAME name for the project. It will create a folder with NAME in current directory. The NAME will also be the module name for the project. OPTION -m, --mono initialize a mono-repo instead a single-repo -a, --monoApp initialize a mono-repo-app instead a single-repo -u, --update update to the latest goframe version -g, --module custom go module -h, --help more information about this command EXAMPLE gf init my-project gf init my-mono-repo -m gf init my-mono-repo -a$ gf init foo -m initializing... initialization done!
在初始化项目目录后,仍然建议升级一下GoFrame版本。
随后需要安装各类驱动组件,首先是微服务组件grpcx
:
1 go get -u github.com/gogf/gf/contrib/rpc/grpcx/v2
其次是数据库驱动
1 go get -u github.com/gogf/gf/contrib/drivers/mysql/v2
附加:数据库驱动组件一览表:
1 2 3 4 5 6 7 8 9 go get -u github.com/gogf/gf/contrib/drivers/mysql/v2# Easy to copy go get -u github.com/gogf/gf/contrib/drivers/clickhouse/v2 go get -u github.com/gogf/gf/contrib/drivers/dm/v2 go get -u github.com/gogf/gf/contrib/drivers/mssql/v2 go get -u github.com/gogf/gf/contrib/drivers/oracle/v2 go get -u github.com/gogf/gf/contrib/drivers/pgsql/v2 go get -u github.com/gogf/gf/contrib/drivers/sqlite/v2 go get -u github.com/gogf/gf/contrib/drivers/sqlitecgo/v2
最后是etcd组件:
1 go get -u github.com/gogf/gf/contrib/registry/etcd/v2
至此,微服务框架建立完毕。
2.微服务编写整体思路 首先,一般一个微服务负责一块业务,如用户账号信息。一个微服务对应一个数据库。在编写业务代码之前,先要设计数据库表格。
在设计完数据库表格之后,可以使用gf init app/user -a
初始化一个新的微服务仓库。
在hack/config.yaml
中配置好数据库连接,执行gf gen dao
即可通过goframe一键生成对应的dao、do和entity实体类。
dao:通过对象方式访问底层数据源,底层基于ORM组件实现。 do:数据转换模型,用于业务模型到数据模型的转换,由工具维护,用户不能修改。工具每次生成代码文件将会覆盖该目录。 entity:数据模型,由工具维护,用户不能修改。工具每次生成代码文件将会覆盖该目录。
由于采用了grpc,需要生成pbentity模型。需要在hack/config.yaml
中增加以下字段:
1 2 pbentity: - link: "mysql:root:12345678@tcp(srv.com:3306)/user"
随后执行gf gen pbentity
即可完成proto文件的生成。
gen pbentity与gen dao的差别 gen dao 生成的数据是go文件,主要在微服务内部使用,例如ORM操作; gen pbentity生成的数据是proto文件,主要用作gRPC微服务之间的通讯。 随后为编写业务逻辑代码,业务逻辑代码存放在*/internal/logic
编写完毕之后,就要开始编写协议文件。
协议文件指的是*.proto文件,proto是gRPC协议通讯的标准,可以类比为json和HTTP。但是千万不要理解proto就是json,他们还是有一定区别的:proto同时定义“接口”信息和响应参数,请求参数,而json就单纯的保存数据。
proto文件统一存放在manifest/protobuf下,和普通的HTTP服务一样,使用目录层级来管理接口版本。
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 syntax = "proto3" ; package account.v1; option go_package = "proxima/app/user/api/account/v1" ; service Account { rpc UserRegister(UserRegisterReq) returns (UserRegisterRes) {} } message UserRegisterReq { string username = 1 ; string password = 2 ; string email = 3 ; } message UserRegisterRes { int32 id = 1 ; }
简单的介绍一下proto
语法:
syntax 规定本文件语法版本;package 定义的是服务命名空间,可以理解为包名;option 设定编译选项,go_package
指定生成的Go
代码所属的包名。在GoFrame
中固定格式是项目名 + app + 微服务名称 + api + 模块名 + v1
;service 定义远程调用方法,一般是RPC
,规定其请求和响应参数;message 定义数据结构,string
是数据类型,username
是字段名,赋值号后面的递增数字是字段编号。最后面的注释是框架提供的参数校检,使用方式普通的HTTP
接口一致 。我们的文件定义了以下内容:
使用proto3
语法版本的定义; 定义了包名为account.v1
; 设置了Go
代码生成的包路径选项go_package
为proxima/app/user/api/account/v1
; 定义了一个Account
服务,包含一个RPC
方法UserRegister
,它接受UserRegisterReq
消息并返回UserRegisterRes
消息; 定义了一个消息类型UserRegisterReq
,包含三个字段:username
(字符串类型,编号为1)password
(字符串类型,编号为2)email
(字符串类型,编号为3) 定义了一个消息类型UserRegisterRes
,包含一个字段: import "pbentity/users.proto";
,代表引入了其他proto文件。引入的这个文件是gf gen pbentity
生成的;pbentity.Users user
调用引入的数据模型,和go的结构体几乎一致。随后编写控制器,微服务也有对应的控制器,相当于是请求过来了之后该如何处理,和MVC三层模型中的controller基本一致,只不过和web不同点在于,其不会对外暴露,只对内暴露,并且通过grpc交互。
在这里补充一下,MVC三层模型并不是 JAVA的“专利”,任何语言都可以完成MVC三层模型。而且也是对自己提个醒,模型等抽象概念,在任何语言中都可以适用,不要被刻板印象所限制!
随后是编写cmd,一般叫做internal/cmd/cmd.go
,也叫做控制器。
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 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 package cmdimport ( "context" "github.com/gogf/gf/contrib/rpc/grpcx/v2" "github.com/gogf/gf/v2/os/gcmd" "google.golang.org/grpc" "proxima/app/word/internal/controller/words" )var ( Main = gcmd.Command{ Name: "main" , Usage: "main" , Brief: "word grpc service" , Func: func (ctx context.Context, parser *gcmd.Parser) (err error ) { c := grpcx.Server.NewConfig() c.Options = append (c.Options, []grpc.ServerOption{ grpcx.Server.ChainUnary( grpcx.Server.UnaryValidate, )}..., ) s := grpcx.Server.New(c) words.Register(s) s.Run() return nil }, } )
随后,再配置一下主入口文件:main.go
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 package main import ( _ "github.com/gogf/gf/contrib/drivers/mysql/v2" "github.com/gogf/gf/contrib/registry/etcd/v2" "github.com/gogf/gf/contrib/rpc/grpcx/v2" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gctx" "proxima/app/user/internal/cmd" ) func main () { var ctx = gctx.New() conf, err := g.Cfg("etcd" ).Get(ctx, "etcd.address" ) if err != nil { panic (err) } var address = conf.String() grpcx.Resolver.Register(etcd.New(address)) cmd.Main.Run(ctx) }
配置文件:
manifest/config/etcd.yaml
1 2 3 etcd: address: "srv.com:2379"
manifest/config/config.yaml
1 2 3 4 5 6 7 8 grpc: name: "user" address: ":32001" database: default: link: "mysql:root:12345678@tcp(srv.com:3306)/user" debug: true
gprc字段定义了两个字段,微服务名称和监听端口。微服务名称会用作服务注册,监听端口不必多言。
3.网关编写整体思路 网关的初始化和前面微服务的初始化无误。但是网关的意义和微服务的意义完全不同。
网关的核心作用:
协议转换:将 HTTP/REST 请求转换为 gRPC 调用,实现协议适配 路由管理:统一管理和转发客户端请求到相应的微服务 流量控制:实现负载均衡、限流、熔断等流量治理功能 统一入口:为所有微服务提供统一的访问入口 故第一步应该先将微服务的各个请求接口写入到网关内,而由于goframe脚手架的存在,在api目录下,只需要写出请求参数和响应参数,通过gf gen ctrl
命令即可生成控制器。
例如:
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 package v1 import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gtime" ) type CreateReq struct { g.Meta `path:"words" method:"post" sm:"创建" tags:"单词"` Word string `json:"word" v:"required|length:1,100" dc:"单词"` Definition string `json:"definition" v:"required|length:1,300" dc:"单词定义"` } type CreateRes struct { } type DetailReq struct { g.Meta `path:"words/{id}" method:"get" sm:"详情" tags:"单词"` Id uint `json:"id" v:"required"` } type DetailRes struct { Id uint `json:"id"` Word string `json:"word"` Definition string `json:"definition"` ExampleSentence string `json:"exampleSentence"` ChineseTranslation string `json:"chineseTranslation"` Pronunciation string `json:"pronunciation"` CreatedAt *gtime.Time `json:"createdAt"` UpdatedAt *gtime.Time `json:"updatedAt"` }
1 2 3 4 5 6 7 8 9 10 11 $ gf gen ctrl generated: D:\project\proxima\app\gateway\api\user\user.go generated: D:\project\proxima\app\gateway\internal\controller\user\user.go generated: D:\project\proxima\app\gateway\internal\controller\user\user_new.go generated: D:\project\proxima\app\gateway\internal\controller\user\user_v1_login.go generated: D:\project\proxima\app\gateway\api\words\words.go generated: D:\project\proxima\app\gateway\internal\controller\words\words.go generated: D:\project\proxima\app\gateway\internal\controller\words\words_new.go generated: D:\project\proxima\app\gateway\internal\controller\words\words_v1_create.go generated: D:\project\proxima\app\gateway\internal\controller\words\words_v1_detail.go done!
随后编写配置文件,将网关正确加入到etcd内。在这里提一句,网关的角色应该为客户端(Client),而微服务的角色应该为服务端(Server)。
在编写好配置文件后,则需要编写cmd文件,在cmd文件内,需要注册其作为路由,以实现统一入口功能。
示例:
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 package cmd import ( "context" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" "github.com/gogf/gf/v2/os/gcmd" "proxima/app/gateway/internal/controller/user" "proxima/app/gateway/internal/controller/words" ) var ( Main = gcmd.Command{ Name: "main" , Usage: "main" , Brief: "start http gateway server" , Func: func (ctx context.Context, parser *gcmd.Parser) (err error ) { s := g.Server() s.Group("/" , func (group *ghttp.RouterGroup) { group.Middleware(ghttp.MiddlewareHandlerResponse) group.Group("/v1" , func (group *ghttp.RouterGroup) { group.Group("/" , func (group *ghttp.RouterGroup) { group.Bind(user.NewV1()) group.Bind(words.NewV1()) }) }) }) s.Run() return nil }, } )
其次也需要添加一个超时拦截器,以保证不会无限制阻塞导致卡死。
拦截器等工具的具体实现则放在utility/
内。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package utility import ( "context" "time" "google.golang.org/grpc" ) func GrpcClientTimeout (ctx context.Context, method string , req, reply interface {}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption, ) error { ctx, cancel := context.WithTimeout(ctx, 3 *time.Second) defer cancel() err := invoker(ctx, method, req, reply, cc, opts...) return err }
在有路由之后,则需要封装路由内的逻辑,也就是如何调度微服务。
我们将在控制器属性里,定义gRPC client,供后续使用。
定义client由grpcx.Client.MustNewGrpcClientConn(service, opts...)
完成。
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package user import ( "github.com/gogf/gf/contrib/rpc/grpcx/v2" "proxima/app/gateway/api/user" "proxima/app/gateway/utility" v1 "proxima/app/user/api/account/v1" ) type ControllerV1 struct { AccountClient v1.AccountClient } func NewV1 () user.IUserV1 { var conn = grpcx.Client.MustNewGrpcClientConn("user" , grpcx.Client.ChainUnary( utility.GrpcClientTimeout, )) return &ControllerV1{ AccountClient: v1.NewAccountClient(conn), } }
接下来,我们要在控制器里调用微服务,完成具体的业务逻辑。
实际开发中,复杂业务逻辑需要封装到logic中,像Web单体服务一样,由控制器调用。
controller/user/user_v1_login.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package user import ( "context" account "proxima/app/user/api/account/v1" "proxima/app/gateway/api/user/v1" ) func (c *ControllerV1) Login(ctx context.Context, req *v1.LoginReq) (res *v1.LoginRes, err error ) { user, err := c.AccountClient.UserLogin(ctx, &account.UserLoginReq{ Username: req.Username, Password: req.Password, }) if err != nil { return nil , err } return &v1.LoginRes{ Token: user.GetToken(), }, nil }
至此,初步网关已经完成。
总结 微服务开发流程 设计数据库表结构 初始化微服务仓库: gf init app/user -a
生成数据访问对象:配置hack/config.yaml
执行gf gen dao
生成dao/do/entity 执行gf gen pbentity
生成proto实体 编写业务逻辑代码 (internal/logic) 编写proto协议文件 (manifest/protobuf) 编写控制器 编写cmd入口文件 配置文件编写: 网关开发流程 初始化网关项目 定义API接口参数 使用gf gen ctrl生成控制器 编写配置文件 编写cmd文件(注册路由) 实现拦截器等工具类 封装微服务调用逻辑: