目录
前言
go-zero完整工程目录结构示例如下:
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 |
mall // 工程名称 ├── common // 通用库 │ ├── randx │ └── stringx ├── go.mod ├── go.sum └── service // 服务存放目录 ├── afterSale │ ├── api │ └── model │ └── rpc ├── cart │ ├── api │ └── model │ └── rpc ├── order // 微服务 │ ├── api // 网关 │ └── model // 数据库模型 │ └── rpc // 服务间的通讯 ├── pay │ ├── api │ └── model │ └── rpc ├── product │ ├── api │ └── model │ └── rpc └── user ├── api ├── cronjob ├── model ├── rmq ├── rpc └── script |
最主要是api网关和rpc服务间的通讯。api对外提供访问,rpc对内微服务间进行通讯。
还有一个goctl
代码生成工具,这是一个很重要的工具。
api网关
首先要写一个xxx.api
后缀为api的文件,来定义api网关。主要定义有:
1、定义路由并与handler(可理解为controller)进行关联。
2、定义每个路由的request(请求)和response(响应)的结构体。
3、定义使用的中间件。
定义好以后使用goctl
根据api文件,自动生成代码,目录结构如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
. ├── etc │ └── greet-api.yaml // 配置文件。主要配置:服务名、IP地址、端口、数据库地址、向etcd服务发现,发现微服务等内容 ├── go.mod // mod文件 ├── greet.api // api描述文件。goctl根据这个文件生成api网关代码。 ├── greet.go // main函数入口 └── internal ├── config │ └── config.go // 配置声明type。这个与etc的yaml要对应,一般yaml有的,这里也要有。 ├── handler // 路由及handler转发。这里一般不需要动。 │ ├── greethandler.go │ └── routes.go ├── logic // 业务逻辑。在这里完善业务逻辑。 │ └── greetlogic.go ├── middleware // 中间件文件。在这里定义中间件。 │ └── greetmiddleware.go ├── svc // 服务上下文,只要在这里注册了,就可以在api网关内部使用。可以注册gorm,其他服务的rpc,就可以调用其它服务的方法。 │ └── servicecontext.go └── types // request、response的struct,根据api自动生成,不建议编辑 └── types.go |
如果要对外提供服务,要在api目录下,指定配置文件启动:
1 |
go run greet.go -f etc/greet.yaml |
rpc
首先要写一个xxx.proto
后缀为proto的文件,来定义rpc。主要定义有:
1、定义提供调用的方法。
2、proto3格式定义调用方法的request与response。
定义好以后使用goctl
根据proto文件,自动生成代码,目录结构如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
. ├── etc │ └── greet.yaml // 配置文件。提供服务的IP地址、端口、向etcd注册服务等内容。 ├── go.mod ├── greet // pb.go文件夹。不需要动。 │ └── greet.pb.go ├── greet.go // main函数 ├── greet.proto // proto 文件。goctl根据此文件生成rpc代码。 ├── greetclient │ └── greet.go // 其它服务想要调用这个rpc的具体方法,是通过调用这个go文件。 └── internal ├── config │ └── config.go // yaml配置对应的实体 ├── logic // 业务代码。在此完善逻辑业务。 │ └── pinglogic.go ├── server // rpc server。不需要动。 │ └── greetserver.go └── svc // 依赖资源 └── servicecontext.go |
安装GO环境,并安装go-zero
项目官网:https://go-zero.dev
项目地址:https://github.com/zeromicro/go-zero
安装Go环境自己找。
创建一个项目文件夹micro,这个项目文件夹里以后会创建多个微服务。在micro目录下执行以下命令
1 2 3 |
micro> go env -w GO111MODULE=on micro> go env -w GOPROXY=https://goproxy.cn,direct micro> go mod init micro |
GO111MODULE=on
:开启模块化。
GOPROXY
:Go设置代理,用于使用国内镜像下载module。
go mod init 项目名称
:初始化项目。
初始化后会在项目下生成go.mod
文件
1 2 |
micro └─ go.mod |
安装go-zreo
1 |
micro> go get -u github.com/tal-tech/go-zero |
安装完后go.mod下会生成 go.sum
的文件
1 2 3 |
micro └─ go.mod └─ go.sum |
所以
go.mod
和go.sum
是用来记录管理安装的依赖模块。如果import
模块变红,可以来这里看一下对应的模块是否安装了。
安装 goctl 工具
这个工具可以快速生成代码
1 2 3 4 5 |
# Go 1.15 及之前版本 GO111MODULE=on GOPROXY=https://goproxy.cn/,direct go get -u github.com/tal-tech/go-zero/tools/goctl # Go 1.16 及以后版本 go install github.com/tal-tech/go-zero/tools/goctl@latest |
安装protoc
到这里下载Githubprotoc
。以Windows为例,下载protoc-3.18.1-win64.zip
,并解压,把bin
文件夹里的protoc.exe
放到Go安装路径的bin
文件夹里。go的bin目录要添加到系统的环境变量里。
查看是否安装成功
1 2 |
shell > protoc --version libprotoc 3.18.1 |
安装protoc-gen-go
用于生成proto代码
1 |
go get -u github.com/golang/protobuf/protoc-gen-go@v1.3.2 |
安装grpc
1 |
go get -u google.golang.org/grpc |
需求:
有两个服务,一个是用户服务,另一个是订单服务。
用户有增删改查,订单也有增删改查的功能,订单的查询与新增都需要获取用户的信息。
API网关
在micro项目下新建文件夹user,作为用户微服务。
生成网关模板
接着在user文件夹下新建api文件夹,在api文件夹下使用goctl
生成api模板文件,即用户服务的网关模板
1 |
micro\user\api> goctl api -o user.api |
在 api模板里你只需要做两件事:
1、定义handler与路由
1 2 3 4 5 6 7 8 9 10 |
// 路由与handler(相当于controller) 映射 service user-api { // GetUser 是handler @handler GetUser // 请求方法 路由(请求参数type) return(响应type) get /users/id/:userId(getUserRequest) returns(getUserResponse) @handler CreateUser post /users/create(createUserRequest) } |
2、每个路由的request与response
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 |
// 定义查询用户GetUser 的request(请求)与response(返回) type ( getUserRequest { // path:"id" 说明这个值是从/user/:id 中名为id取得; // 如果是从表单或者user?id=1获取的,使用form id int64 `path:"id"` } // 定义响应体 getUserResponse { // json: json格式的字段名 id int64 `json:"id"` name string `json:"name"` } ) // 定义创建用户CreateUser 的request(请求)与response(返回) type ( createUserRequest { name string `form:"name"` } createUserResponse { ok bool `json:"ok"` } ) |
根据模板生成网关代码
使用goctl
根据api模板生成Gateway代码
1 |
micro\user\api> goctl api go -api user.api -dir . |
1 |
goctl api[api网关操作] go[生成代码语言] -api[指定api模板] user.api[api模板文件] -dir[指定代码生成路径] .[当前路径] |
会生成如下目录结构
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 |
\micro\user\api │ user.api // api模板 │ user.go // 网关入口,main方法 │ ├─etc // etc配置,用于服务发现 │ user-api.yaml │ └─internal // 业务 ├─config // 配置 │ config.go │ ├─handler // 相当于MVC的controller │ createuserhandler.go │ getuserhandler.go │ routes.go // 路由 │ ├─logic // 业务逻辑,相当于MVC的service │ createuserlogic.go │ getuserlogic.go │ ├─svc // 远程调用,跨服务调用 │ servicecontext.go │ └─types // 定义请求、返回结构体 types.go |
etc/user-api.yam
l配置
1 2 3 |
Name: user-api # 网关名称 Host: 0.0.0.0 # 网关IP Port: 8888 # 网关端口 |
如果没有什么需求,直接在logic里完善业务逻辑即可。
例如,完善micro\user\api\internal\logic\getuserlogic.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 |
package logic import ( "context" "micro/user/api/internal/svc" "micro/user/api/internal/types" "github.com/tal-tech/go-zero/core/logx" ) type GetUserLogic struct { logx.Logger // 日志 ctx context.Context // 请求上下文 svcCtx *svc.ServiceContext // 服务上下文 } // go 没有构造器,这个函数作为构造函数,但是需要手动调用 func NewGetUserLogic(ctx context.Context, svcCtx *svc.ServiceContext) GetUserLogic { return GetUserLogic{ Logger: logx.WithContext(ctx), ctx: ctx, svcCtx: svcCtx, } } // 完善你的业务逻辑 func (l *GetUserLogic) GetUser(req types.GetUserRequest) (*types.GetUserResponse, error) { // 从request中取得参数 userId := req.Id // 业务处理 if userId ==1 { return &types.GetUserResponse{ Id: 1, Name: "user Tom", }, nil } return nil, nil } |
同理创建一个order微服务。
启动api网关
就是记得在order/api/etc/order-api.yaml
文件修改端口,不要和user服务的端口冲突了。
启动user和order两个api网关
1 2 3 |
micro\user\api> go run user.go .\etc\user-api.yaml micro\order\api> go run order.go -f .\etc\order-api.yaml |
1 |
micro\order\api> go run order.go[api的入口文件] -f .\etc\order-api.yaml[指定api网关配置文件] |
分别访问 http://localhost:8881/order/id/1
和 http://localhost:8888/users/id/1
即可得到结果。
RPC
创建rpc,用于服务间调用
创建RPC模板
在user文件夹下创建rpc文件夹,user/rpc
就存放着rpc的代码。然后使用goctl
创建rpc模板:
1 |
micro\user\rpc> goctl rpc template -o user.proto |
1 2 3 4 5 6 7 8 9 |
micro │ go.mod │ go.sum │ └─user ├─api │ // api文件夹里的内容省略 └─rpc user.proto |
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 |
syntax = "proto3"; // 使用proto3的语法,一般无需更改 package user; // 定义包名,一般无需更改 message GetUserRequest { // 定义request,接收的结构 int64 id = 1; } message GetUserResponse { // 定义response,响应的结构 int64 id = 1; string name = 2; } message CreateUserRequest { string name = 1; } message CreateUserResponse { bool ok = 1; } service user { // rpc 服务的名称 // rpc的路由、Request、Response rpc getUser(GetUserRequest) returns(GetUserResponse); rpc createUser(CreateUserRequest) returns(CreateUserResponse); } |
根据模板生成代码
使用goctl
生成rpc的代码
1 |
micro\user\rpc> goctl rpc proto -src user.proto -dir . |
生成的目录与文件如下
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 |
micro\user\rpc │ user.go // main函数,入口文件 │ user.proto // rpc模板文件 │ ├─etc // 向etcd注册服务 │ user.yaml │ ├─internal │ ├─config │ │ config.go │ │ │ ├─logic // 业务逻辑 │ │ createuserlogic.go │ │ getuserlogic.go │ │ │ ├─server │ │ userserver.go │ │ │ └─svc │ servicecontext.go // 依赖服务,调用其它服务 │ ├─user // proto 生成的,无需理会 │ user.pb.go │ └─userclient user.go |
user/rpc/etc
配置文件
1 2 3 4 5 6 |
Name: user.rpc # rpc 的名称 ListenOn: 127.0.0.1:8080 # rpc 服务的地址与端口 Etcd: # 向etcd 注册服务 Hosts: # etcd 地址与端口 - 127.0.0.1:2379 Key: user.rpc # 向etcd注册的服务名称 |
填充业务代码 user/rpc/internal/logic
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 |
package logic import ( "context" "micro/user/rpc/internal/svc" "micro/user/rpc/user" "github.com/tal-tech/go-zero/core/logx" ) type GetUserLogic struct { ctx context.Context svcCtx *svc.ServiceContext logx.Logger } func NewGetUserLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetUserLogic { return &GetUserLogic{ ctx: ctx, svcCtx: svcCtx, Logger: logx.WithContext(ctx), } } func (l *GetUserLogic) GetUser(in *user.GetUserRequest) (*user.GetUserResponse, error) { userId := in.Id if userId == 1 { return &user.GetUserResponse{ Id: 1, Name: "user rpc Tom", }, nil } return nil, nil } |
user/rpc/internal/config/config.go
1 2 3 4 5 6 7 8 |
package config import "github.com/tal-tech/go-zero/zrpc" type Config struct { // 这个是 etcd的yaml配置实体 zrpc.RpcServerConf } |
以同样的方式创建order订单服务rpc服务。
配置依赖关系
假设order服务的api网关的getOrder方法,需要获取user服务的RPC提供的getUser方法,来获取user信息。
1、order在配置中添加user服务的RPC配置order/api/internal/config/config.go
1 2 3 4 5 6 7 8 9 10 11 |
package config import ( "github.com/tal-tech/go-zero/rest" "github.com/tal-tech/go-zero/zrpc" ) type Config struct { rest.RestConf UserRpc zrpc.RpcClientConf // user的RPC客户端配置 } |
2、配置etcd,发现服务 order/api/etc/order-api.yaml
1 2 3 4 5 6 7 8 |
Name: order-api # api网关名称 Host: 0.0.0.0 # api网关地址 Port: 8881 # api网关端口 UserRpc: # 依赖的服务RPC Etcd: Hosts: # etcd地址 - 127.0.0.1:2379 Key: user.rpc # 依赖的服务RPC在etcd的名称 |
3、创建服务的RPC实例到ServiceContext
(服务上下文)。order/api/internal/svc/servicecontext.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
package svc import ( "github.com/tal-tech/go-zero/zrpc" "micro/order/api/internal/config" "micro/user/rpc/userclient" ) type ServiceContext struct { Config config.Config UserRpc userclient.User // 引入user RPC接口,路径user/rpc/userclient/user.go } func NewServiceContext(c config.Config) *ServiceContext { return &ServiceContext{ Config: c, // 实例 user RPC ,并放到ServiceContext(服务上下文中) UserRpc: userclient.NewUser(zrpc.MustNewClient(c.UserRpc)), } } |
4、调用RPC完成业务逻辑
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 |
package logic import ( "context" "errors" "micro/user/rpc/userclient" "micro/order/api/internal/svc" "micro/order/api/internal/types" "github.com/tal-tech/go-zero/core/logx" ) type GetOrderLogic struct { logx.Logger ctx context.Context svcCtx *svc.ServiceContext } func NewGetOrderLogic(ctx context.Context, svcCtx *svc.ServiceContext) GetOrderLogic { return GetOrderLogic{ Logger: logx.WithContext(ctx), ctx: ctx, svcCtx: svcCtx, } } func (l *GetOrderLogic) GetOrder(req types.GetOrderRequest) (*types.GetOrderResponse, error) { // 获取参数 orderId := req.Id if orderId <= 0 { return nil, errors.New("订单id不能为空") } // 调用user的RPC,通过UserRpc提供的方法获取user服务的数据。 // user的RPC的实例在order/api/internal/svc/servicecontext.go 已注册到了ServiceContext上下文中。 user, err := l.svcCtx.UserRpc.GetUser(l.ctx, &userclient.GetUserRequest{ // user GetUser方法请求参数 Id: orderId, }) if err != nil { return nil, err } return &types.GetOrderResponse{ OrderId: req.Id, UserName: user.Name, }, nil } |
启动rpc
注意:如果api网关依赖了RPC,要RPC先启动,注册到etcd上。
1 |
micro\user\rpc> go run user.go -f etc/user.yaml |
1 |
go run user.go[user服务RPC的入口] -f etc/user.yaml[user PRC 配置文件] |
order的api网关服务也要启动起来,通过地址访问http://localhost:8881/order/id/1
,返回的username是rpc获取到的数据。
model
在user服务文件夹下新建model文件夹。
在model创建的user服务数据库sql文件。(一个微服务有一个对应的数据库)
1 2 3 4 5 6 |
CREATE TABLE if not exists `user` ( `id` bigint(20) NOT NULL auto_increment COMMENT '用户id', `name` varchar(100) NOT NULL default '' COMMENT '用户名称', PRIMARY KEY(`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; |
使用goctl命令生成CRUD+cache代码
1 |
\micro\user\model> goctl model mysql ddl -c -src micro_user.sql -dir . |
1 |
goctl model mysql[数据库类型] ddl -c[使用redis cache] -src micro_user.sql[依据的sql文件] -dir .[代码生成的路径] |
1 2 3 4 |
user/model ├── micro_user.sql ├── usermodel.go // CRUD+cache代码 └── vars.go // 定义常量和变量 |
配置
1、配置yaml
api和rpc 那个需要数据库就在对应的etc/xxx.yaml
配置数据库
例如:rpc需要用到数据库,就在user/rpc/etc/user.yaml
添加数据库配置。
1 2 3 4 5 6 7 8 9 10 11 |
Name: user.rpc # rpc 的名称 ListenOn: 127.0.0.1:8080 # rpc 服务的地址与端口 Etcd: # 向etcd 注册服务 Hosts: # etcd 地址与端口 - 192.168.5.65:2379 Key: user.rpc # 向etcd注册的服务名称 DataSource: root:123456@tcp(192.168.5.65:3306)/micro_user # 数据库链接 Table: user # 使用的表 Cache: # 使用缓存 - Host: 192.168.5.65:6379 # redis地址端口 Pass: 123456 # redis密码 |
2、到user/rpc/internal/config/config.go
配置实例配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
package config import ( "github.com/tal-tech/go-zero/core/stores/cache" "github.com/tal-tech/go-zero/zrpc" ) // 这个和 etc/xxx.yaml的结构体 type Config struct { // 这个是 etcd的yaml配置实体 zrpc.RpcServerConf DataSource string // 数据库实例 Cache cache.CacheConf // 缓存实例 } |
3、注册到服务上下文中 user/rpc/internal/svc/servicecontext.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
package svc import ( "github.com/tal-tech/go-zero/core/stores/sqlx" "micro/user/model" "micro/user/rpc/internal/config" ) type ServiceContext struct { Config config.Config Model model.UserModel // 引入了 model/usermodel.go } func NewServiceContext(c config.Config) *ServiceContext { return &ServiceContext{ Config: c, Model: model.NewUserModel(sqlx.NewMysql(c.DataSource), c.Cache), // 实例user model } } |
4、 业务使用数据库 user/rpc/internal/logic/getuserlogic.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 |
package logic import ( "context" "micro/user/rpc/internal/svc" "micro/user/rpc/user" "github.com/tal-tech/go-zero/core/logx" ) type GetUserLogic struct { ctx context.Context svcCtx *svc.ServiceContext logx.Logger } func NewGetUserLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetUserLogic { return &GetUserLogic{ ctx: ctx, svcCtx: svcCtx, Logger: logx.WithContext(ctx), } } func (l *GetUserLogic) GetUser(in *user.GetUserRequest) (*user.GetUserResponse, error) { // l.svcCtx.Model 其中Model是已经在 rpc/internal/svc/servicecontext.go 服务上下文中注册了的 // 查询 userData, err := l.svcCtx.Model.FindOne(in.Id) if err != nil { return nil, err } return &user.GetUserResponse{ Id: userData.Id, Name: userData.Name, }, nil /* userId := in.Id if userId == 1 { return &user.GetUserResponse{ Id: 1, Name: "user rpc Tom", }, nil } return nil, nil*/ } |
启动rpc去查询即可。
集成gorm
先安装gorm 以及 对应数据库的驱动
1 2 |
go get -u gorm.io/gorm go get -u gorm.io/driver/mysql |
1、在etc/xxx.yaml
配置数据库连接。api网关和rpc那个需要就到对应的etc文件夹下配置。
这里是在API网关下配置的 user/api/etc/user-api.yaml
1 2 3 4 |
Name: user-api # 网关名称 Host: 0.0.0.0 # 网关IP Port: 8888 # 网关端口 DataSource: root:123456@tcp(192.168.5.65:3306)/micro_user # 数据库链接 |
2、配置结构体
1 2 3 4 5 6 7 8 9 10 11 |
package config import ( "github.com/tal-tech/go-zero/rest" ) // Config结构体是etc/xxx.yaml的go表现形式 type Config struct { rest.RestConf DataSource string // 数据库实例 } |
3、注册到服务上下文 user/api/internal/svc/servicecontext.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 |
package svc import ( "gorm.io/gorm" "gorm.io/gorm/schema" "micro/user/api/internal/config" "micro/user/model" "gorm.io/driver/mysql" ) type ServiceContext struct { Config config.Config DB *gorm.DB // 引入gorm } func NewServiceContext(c config.Config) *ServiceContext { return &ServiceContext{ Config: c, DB: initGorm(c.DataSource), // 实例gorm } } func initGorm(dataSourceName string) *gorm.DB { //启动Gorm支持 db, err := gorm.Open(mysql.Open(dataSourceName), &gorm.Config{ NamingStrategy: schema.NamingStrategy{ //TablePrefix: "tech_", // 表名前缀,`User` 的表名应该是 `t_users` SingularTable: true, // 使用单数表名,启用该选项,此时,`User` 的表名应该是 `t_user` }, }) //如果出错就GameOver了 if err != nil { panic(err) } //自动同步更新表结构,不要建表了O(∩_∩)O哈哈~ db.AutoMigrate(&model.User{}) return db } |
4、新建user模型文件 user/model/user.go
1 2 3 4 5 6 7 8 |
package model import "gorm.io/gorm" type User struct { gorm.Model Name string `gorm:"type:varchar(100) not null default '' comment '用户名称'"` } |
5、实现业务逻辑user/api/internal/logic/getuserlogic.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 |
package logic import ( "context" "micro/user/api/internal/svc" "micro/user/api/internal/types" "micro/user/model" "github.com/tal-tech/go-zero/core/logx" ) type GetUserLogic struct { logx.Logger // 日志 ctx context.Context // 请求上下文 svcCtx *svc.ServiceContext // 服务上下文 } // go 没有构造器,这个函数作为构造函数,但是需要手动调用 func NewGetUserLogic(ctx context.Context, svcCtx *svc.ServiceContext) GetUserLogic { return GetUserLogic{ Logger: logx.WithContext(ctx), ctx: ctx, svcCtx: svcCtx, } } // 完善你的业务逻辑 func (l *GetUserLogic) GetUser(req types.GetUserRequest) (*types.GetUserResponse, error) { var userModel model.User // 查询 userDB := l.svcCtx.DB.Where("id=?",req.Id).Find(&userModel) if userDB.Error != nil { return nil, userDB.Error } return &types.GetUserResponse{ Id: int64(userModel.ID), Name: userModel.Name, }, nil } |
user/api/internal/logic/createuserlogic.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 |
package logic import ( "context" "github.com/tal-tech/go-zero/core/logx" "micro/user/api/internal/svc" "micro/user/api/internal/types" "micro/user/model" ) type CreateUserLogic struct { logx.Logger ctx context.Context svcCtx *svc.ServiceContext } func NewCreateUserLogic(ctx context.Context, svcCtx *svc.ServiceContext) CreateUserLogic { return CreateUserLogic{ Logger: logx.WithContext(ctx), ctx: ctx, svcCtx: svcCtx, } } func (l *CreateUserLogic) CreateUser(req types.CreateUserRequest) (*types.CreateUserResponse, error) { userModel := model.User{Name: req.Name} // 新增数据 userDB := l.svcCtx.DB.Create(&userModel) if userDB.Error != nil { return nil, userDB.Error } return &types.CreateUserResponse{ Ok: true, }, nil } |
jwt
go-zero自带jwt.
1、编写api网关模板 user/api/user.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 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
syntax = "v1" info( title: "user service gateway" desc: "user service gateway" author: "admin" email: "admin@qq.com" ) type ( getUserRequest { Id int64 `path:"userId"` } getUserResponse { Id int64 `json:"id"` Name string `json:"name"` } ) type ( createUserRequest { Name string `form:"name"` Email string `form:"email"` Password string `form:"password"` } createUserResponse { Ok bool `json:"ok"` } ) type ( loginRequest { Email string `form:"email"` Password string `form:"password"` } loginResponse { Expire int64 `json:"expire"` Token string `json:"token"` } ) // 以下路由需要token验证 @server ( jwt: Auth // 声明使用jwt ) service user-api { @handler GetUser get /users/id/:userId(getUserRequest) returns(getUserResponse) } // 不需要token service user-api { @handler Login post /login(loginRequest) returns(loginResponse) @handler CreateUser post /users/create(createUserRequest) returns(createUserResponse) } |
生成代码
1 |
micro\user\api> goctl api go -api user.api -dir . |
2、配置yaml user/api/etc/user-api.yaml
配置Auth
标签。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
Name: user-api # 网关名称 Host: 0.0.0.0 # 网关IP Port: 8888 # 网关端口 DataSource: root:123456@tcp(192.168.5.65:3306)/micro_user?charset=utf8mb4&parseTime=true # 数据库链接 Table: user # 使用的表 Cache: # 使用缓存 - Host: 192.168.5.65:6379 # redis地址端口 Pass: "123456" # redis密码 Auth: # 生成jwt token的密钥,最简单的方式可以使用一个uuid值 AccessSecret: b6957733-ac6f-4066-aefe-6a89263ca2f9 # jwt token有效期,单位:秒 AccessExpire: 1800 |
3、配置config user/api/internal/config/config.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
package config import ( "github.com/tal-tech/go-zero/core/stores/cache" "github.com/tal-tech/go-zero/rest" ) // Config结构体是etc/xxx.yaml的go表现形式 type Config struct { rest.RestConf DataSource string // 数据库实例 Cache cache.CacheConf // 缓存实例 Auth struct { // jwt的配置 AccessSecret string AccessExpire int64 } } |
4、编写登录逻辑 user/api/internal/logic/loginlogic.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 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 |
package logic import ( "context" "errors" "github.com/golang-jwt/jwt" "github.com/tal-tech/go-zero/core/logx" "micro/user/api/internal/svc" "micro/user/api/internal/types" "micro/user/model" "strings" "time" ) type LoginLogic struct { logx.Logger ctx context.Context svcCtx *svc.ServiceContext } func NewLoginLogic(ctx context.Context, svcCtx *svc.ServiceContext) LoginLogic { return LoginLogic{ Logger: logx.WithContext(ctx), ctx: ctx, svcCtx: svcCtx, } } // 登录 func (l *LoginLogic) Login(req types.LoginRequest) (*types.LoginResponse, error) { if len(strings.TrimSpace(req.Email)) == 0 || len(strings.TrimSpace(req.Password)) == 0 { return nil, errors.New("参数错误") } var userInfo model.User result := l.svcCtx.DB.Where("email=?",req.Email).Find(&userInfo) // 查询报错 if result.Error != nil { return nil,result.Error } if userInfo.ID == 0 { return nil,errors.New("用户不存在") } if userInfo.Password != req.Password { return nil, errors.New("用户密码不正确") } now := time.Now().Unix() accessExpire := l.svcCtx.Config.Auth.AccessExpire jwtToken, jwtErr := l.getJwtToken(l.svcCtx.Config.Auth.AccessSecret, now, l.svcCtx.Config.Auth.AccessExpire, int64(userInfo.ID)) if jwtErr != nil { return nil, jwtErr } return &types.LoginResponse{ Token: jwtToken, Expire: now + accessExpire, }, nil } // 获取token func (l *LoginLogic) getJwtToken(secretKey string, iat, seconds, userId int64) (string, error) { claims := make(jwt.MapClaims) claims["exp"] = iat + seconds claims["iat"] = iat claims["userId"] = userId // 把user id 放到token中 token := jwt.New(jwt.SigningMethodHS256) token.Claims = claims return token.SignedString([]byte(secretKey)) } |
启动访问
1 |
micro\user\api> go run user.go -f etc/user-api.yaml |
获取jwt token中携带的信息
go-zero从jwt token解析后会将用户生成token时传入的kv原封不动的放在http.Request
的Context
中,因此我们可以通过Context
就可以拿到你想要的值。
获取用户信息时,获取token携带的信息 user/api/internal/logic/getuserlogic.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 |
package logic import ( "context" "micro/user/api/internal/svc" "micro/user/api/internal/types" "micro/user/model" "github.com/tal-tech/go-zero/core/logx" ) type GetUserLogic struct { logx.Logger ctx context.Context svcCtx *svc.ServiceContext } func NewGetUserLogic(ctx context.Context, svcCtx *svc.ServiceContext) GetUserLogic { return GetUserLogic{ Logger: logx.WithContext(ctx), ctx: ctx, svcCtx: svcCtx, } } func (l *GetUserLogic) GetUser(req types.GetUserRequest) (*types.GetUserResponse, error) { // 获取token的值 logx.Infof("userId: %v",l.ctx.Value("userId"))// 这里的key和生成jwt token时传入的key一致 var userModel model.User userDB := l.svcCtx.DB.Where("id=?",req.Id).Find(&userModel) if userDB.Error != nil { return nil, userDB.Error } return &types.GetUserResponse{ Id: int64(userModel.ID), Name: userModel.Name, }, nil } |
token必须在header以
Authorization:bearer
携带
、
中间件
go-zero的中间件有路由中间件
和全局中间件
。
路由中间件
1、编写api网关模板,声明中间件 user/api/user.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 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
syntax = "v1" info( title: "user service gateway" desc: "user service gateway" author: "admin" email: "admin@qq.com" ) type ( getUserRequest { Id int64 `path:"userId"` } getUserResponse { Id int64 `json:"id"` Name string `json:"name"` } ) type ( createUserRequest { Name string `form:"name"` Email string `form:"email"` Password string `form:"password"` } createUserResponse { Ok bool `json:"ok"` } ) type ( loginRequest { Email string `form:"email"` Password string `form:"password"` } loginResponse { Expire int64 `json:"expire"` Token string `json:"token"` } ) @server ( jwt: Auth middleware: My // 声明使用自定义的,名字为My的中间件 ) service user-api { @handler GetUser get /users/id/:userId(getUserRequest) returns(getUserResponse) } service user-api { @handler Login post /login(loginRequest) returns(loginResponse) @handler CreateUser post /users/create(createUserRequest) returns(createUserResponse) } |
生成代码
1 |
micro\user\api> goctl api go -api user.api -dir . |
生成完后会在internal
目录下多一个middleware
的目录,这里即中间件文件,后续中间件的实现逻辑也在这里编写。
2、把中间件注册到服务上下文ServiceContext user/api/internal/svc/servicecontext.go
1 2 3 4 5 6 7 8 9 10 11 |
type ServiceContext struct { Config config.Config My rest.Middleware // 引入自定义中间件 } func NewServiceContext(c config.Config) *ServiceContext { return &ServiceContext{ Config: c, My: middleware.NewExampleMiddleware().Handle, // 实例自定义中间件 } } |
3、编写中间件逻辑 user/api/internal/middleware/mymiddleware.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 middleware import ( "github.com/tal-tech/go-zero/core/logx" "net/http" ) type MyMiddleware struct { } func NewMyMiddleware() *MyMiddleware { return &MyMiddleware{} } // 中间件业务逻辑处理 func (m *MyMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // 这里编写业务逻辑 logx.Info("这里是自定义中间件My在处理业务。。。") // next(w, r)表示放行,不通过就不要调用此方法 next(w, r) } } |
运行
1 |
micro\user\api> go run user.go -f etc/user-api.yaml |
访问http://localhost:8888/users/id/1
(因为定义了在这个路由上应用中间件),在控制台会输出中间里写的业务
1 |
{"@timestamp":"2021-10-22T09:27:56.329+08","level":"info","content":"这里是自定义中间件My在处理业务。。。"} |
全局中间件
在api网关入口文件中 user/api/user.go
, 通过rest.Server
提供的Use
方法即可。
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 |
package main import ( "flag" "fmt" "github.com/tal-tech/go-zero/core/logx" "net/http" "micro/user/api/internal/config" "micro/user/api/internal/handler" "micro/user/api/internal/svc" "github.com/tal-tech/go-zero/core/conf" "github.com/tal-tech/go-zero/rest" ) var configFile = flag.String("f", "etc/user-api.yaml", "the config file") func main() { flag.Parse() var c config.Config conf.MustLoad(*configFile, &c) ctx := svc.NewServiceContext(c) server := rest.MustNewServer(c.RestConf) defer server.Stop() // 全局中间件 server.Use(func(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { logx.Info("这里是一个全局中间件") next(w, r) } }) handler.RegisterHandlers(server, ctx) fmt.Printf("Starting server at %s:%d...\n", c.Host, c.Port) server.Start() } |
启动
1 |
micro\user\api> go run user.go -f etc/user-api.yaml |
访问任意路由,在控制台输出全局中间件的提示
1 |
{"@timestamp":"2021-10-22T09:37:07.044+08","level":"info","content":"这里是一个全局中间件"} |
错误处理
go-zero错误信息都是以plain text
形式返回的,现在以json响应体来进行响应。
1、在项目新建common
文件夹,与各个服务文件夹同级,是各个服务的公共文件夹。
1 2 3 4 5 6 7 8 9 |
micro ├─common // 各个服务的共用文件夹 │ ├─order // 订单服务 │ ├─api │ └─rpc └─user // 用户服务 ├─api └─rpc |
在common
文件夹下新建errorx
文件夹,用于存放自定义错误。并在errorx
文件夹下添加一个baseerror.go
文件,并填入代码。
路径:common/errorx/baseerror.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 |
package errorx const defaultCode = 1001 // 定义错误结构体 type CodeError struct { Code int `json:"code"` Msg string `json:"msg"` } // 定义错误响应结构体 type CodeErrorResponse struct { Code int `json:"code"` Msg string `json:"msg"` } // 错误构造 func NewCodeError(code int, msg string) error { return &CodeError{Code: code, Msg: msg} } // 默认错误构造 func NewDefaultError(msg string) error { return NewCodeError(defaultCode, msg) } func (e *CodeError) Error() string { return e.Msg } func (e *CodeError) Data() *CodeErrorResponse { return &CodeErrorResponse{ Code: e.Code, Msg: e.Msg, } } |
2、逻辑中错误用CodeError
自定义错误替换
比如,改造登录逻辑 user/api/internal/logic/loginlogic.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 |
// 登录 func (l *LoginLogic) Login(req types.LoginRequest) (*types.LoginResponse, error) { if len(strings.TrimSpace(req.Email)) == 0 || len(strings.TrimSpace(req.Password)) == 0 { // 使用了自定义错误 return nil, errorx.NewDefaultError("参数错误") } var userInfo model.User result := l.svcCtx.DB.Where("email=?",req.Email).Find(&userInfo) // 查询报错 if result.Error != nil { return nil,result.Error } if userInfo.ID == 0 { return nil, errorx.NewDefaultError("用户不存在") } if userInfo.Password != req.Password { return nil, errorx.NewDefaultError("用户密码不正确") } now := time.Now().Unix() accessExpire := l.svcCtx.Config.Auth.AccessExpire jwtToken, jwtErr := l.getJwtToken(l.svcCtx.Config.Auth.AccessSecret, now, l.svcCtx.Config.Auth.AccessExpire, int64(userInfo.ID)) if jwtErr != nil { return nil, jwtErr } return &types.LoginResponse{ Token: jwtToken, Expire: now + accessExpire, }, nil } |
3、在服务入口处,开启自定义错误 user/api/user.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 |
func main() { flag.Parse() var c config.Config conf.MustLoad(*configFile, &c) ctx := svc.NewServiceContext(c) server := rest.MustNewServer(c.RestConf) defer server.Stop() handler.RegisterHandlers(server, ctx) // 自定义错误 httpx.SetErrorHandler(func(err error) (int, interface{}) { switch e := err.(type) { case *errorx.CodeError: // 当错误类型是自定义错误的处理 return http.StatusOK, e.Data() default: return http.StatusInternalServerError, nil } }) fmt.Printf("Starting server at %s:%d...\n", c.Host, c.Port) server.Start() } |
启动
1 |
micro\user\api> go run user.go -f etc/user-api.yaml |
访问login路由,输错密码,会返回如下信息
1 2 3 4 |
{ "code": 1001, "msg": "用户密码不正确" } |