go-zero

目录

  • 引入
    • 开发派系
      • 标准库/自研派系——不要让框架束缚开发
      • web框架派系——gin+grpc
      • 大一统框架
  • go-zero
  • go-zero快速实现一个微服务
    • user service
    • order api server
    • 启动
  • goctl
    • 安装
    • 生成的api网关目录
    • 生成的pb目录
    • api语法
      • syntax
      • import语法块
      • info
      • type
      • service
      • 注释
    • 命令大全
      • api命令
      • rpc命令
        • proto import
      • model命令
  • go-zero使用
    • 集成mongodb
    • 集成jwt和中间件
    • 自定义错误
    • 修改模板
    • 集成zap日志
    • 集成[prometheus](https://www.prometheus.wang/)
    • 分布式链路追踪——[Jaeger](https://www.jaegertracing.io/download/)
    • 分布式事务(DTM)
      • 原则上能不用就不用
      • 更多应用场景
      • 使用dtm
  • 代码存储

引入

go-zero_第1张图片
该图片来自微软开源的一个容器商城项目:eShopOnContainers,其中红框部分就是各个微服务模块。

每一个微服务都是一个独立的生态,比较直观的一点是各自拥有独立的数据库。

微服务化后,带来了诸多好处,比如弹性,敏捷,灵活扩展,易于部署,可重用代码等。

但也带来了复杂性,让整个架构变得不易于维护,所以诞生出来很多组件用于辅助,这些组件同样成为了微服务架构中的一员:

  1. 服务网关:确保服务提供者对客户端的透明,这一层可以进行反向路由,安全认证,灰度发布,日志监控等前置动作。
    go-zero_第2张图片
  2. 服务发现:注册并维护远程服务及服务提供者的地址,供服务消费者发现和调用,如:etcd,consul等
  3. 服务框架:实现了rpc框架,包含服务接口描述和实现,向注册中心发布服务等功能,比如grpc等
  4. 服务监控:对服务消费者与提供者之间的的调用情况进行监控和数据展示比如:普罗米修斯(prometheus)
  5. 服务追踪:记录每个请求的为服务器调用完整链路,以便进行问题定位和故障分析,比如jeager,zipkin等
  6. 服务治理:服务治理就是通过一系列的手段来保证在各种意外情况下,服务调用仍然能够正常进行,这些手段包括熔断,隔离,限流,降级,负载均衡等,比如:Sentinel等.
  7. 基础设施:用以提供服务底层的基础数据服务,比如分布式消息队列,日志存储,数据库,缓存,文件服务器,搜索集群。比如:kafka,mysql,pgsql,mongodb,redis,minio,elasticSearch等。
  8. 分布式配置中心:统一配置,比如nacos,consul,zk,apollo等
  9. 分布式事务:dtm,seata等
  10. 容器以及容器编排:docker,k8s等
  11. 定时任务

综上,容器时微服务架构的绝佳示例,现代云原生应用使用容器来构建微服务。

开发派系

标准库/自研派系——不要让框架束缚开发

  1. 对于go标准库的强大(稳定,高性能),让很多开发者不使用框架也可以写出高效的应用程序。
  2. 微服务的基础是通信,也就是rpc框架的选择,大部分会选择grpc或者基于grpc的基础进行自研rpc框架的开发
  3. 其他组件需要的时候,进行集成就可以了,而不是非得用某个框架定义的组件。
  4. 如果部署采用k8s,并且使用服务网格,比如Istio来处理,那么开发者只需要关心业务逻辑即可,不需要关心服务发现,熔断,流量控制,负载均衡。

web框架派系——gin+grpc

  1. 由于标准库到web框架开发仍然需要一定量的开发工作,所以选择成熟的gin框架,出现了grpc+gin核心,其他组件集成进来的微服务框架。
  2. gin在这里可以作为grpc网关使用,写一些限流中间件,认证中间件。通过在api view中调用内部的微服务,对外提供服务。
  3. 同样可以使用k8s+istio.

大一统框架

  1. 使用框架能减轻工作量,达到快速开发的目的。代价就是遵循框架的规则。
  2. go的微服务框架比较多,如:go-zero,go-micro,go-kit…
  3. go-zero是一个不错的选择,其社区活跃,文档齐全。

go-zero

go-zero_第3张图片

go-zero 是一个集成了各种工程实践的包含 web 和 rpc 框架,有如下主要特点:

  1. 强大的工具支持,尽可能少的代码编写
  2. 极简的接口
  3. 完全兼容 net/http
  4. 支持中间件,方便扩展
  5. 高性能
  6. 面向故障编程,弹性设计
  7. 内建服务发现、负载均衡
  8. 内建限流、熔断、降载,且自动触发,自动恢复
  9. API 参数自动校验
  10. 超时级联控制
  11. 自动缓存控制
  12. 链路跟踪、统计报警等
  13. 高并发支撑,稳定保障了疫情期间每天的流量洪峰

go-zero_第4张图片

go-zero快速实现一个微服务

  1. 订单服务(order)提供一个查询接口
  2. 用户服务(user)提供一个方法供订单服务获取用户信息

user service

  1. 创建user目录

  2. 编写proto文件:

    syntax = "proto3";
    
    package user;
    
    option go_package = "./user";
    
    message IdRequest {
      string id = 1;
    }
    
    message UserResponse {
      // 用户id
      string id = 1;
      // 用户名称
      string name = 2;
      // 用户性别
      string gender = 3;
    }
    
    service User {
      rpc getUser(IdRequest) returns(UserResponse);
    }
    
  3. 使用goctl生成go-zero模板和pb文件:goctl rpc protoc user.proto --go_out=./types --go-grpc_out=./types --zrpc_out=.

  4. 实现查询用户的功能:

    func (l *GetUserLogic) GetUser(in *user.IdRequest) (*user.UserResponse, error) {
    	return &user.UserResponse{
    		Id:     in.Id,
    		Name:   "generalzy",
    		Gender: "武装直升机",
    	}, nil
    }
    

    go-zero_第5张图片

  5. go build编译

order api server

  1. 创建order目录

  2. 编写api文件:

    type (
    	OrderReq {
    		Id string `path:"id"`
    	}
    
    	OrderReply {
    		Id   string `json:"id"`
    		Name string `json:"name"`
    	}
    )
    service order {
    	@handler getOrder
    	get /api/order/get/:id (OrderReq) returns (OrderReply)
    }
    
  3. 使用goctl生成api:goctl api go -api order.api -dir .

  4. 配置user config:

    type Config struct {
        rest.RestConf
        UserRpc zrpc.RpcClientConf
    }
    

    go-zero_第6张图片

  5. 配置yaml:

    Name: order
    Host: 0.0.0.0
    Port: 8888
    UserRpc:
      Etcd:
        Hosts:
          - 127.0.0.1:2379
        Key: user.rpc
    
  6. 完善服务依赖(将user放入到order的上下文)

    type ServiceContext struct {
    	Config  config.Config
    	UserRpc user.User
    }
    
    func NewServiceContext(c config.Config) *ServiceContext {
    	return &ServiceContext{
    		Config:  c,
    		UserRpc: user.NewUser(zrpc.MustNewClient(c.UserRpc)),
    	}
    }
    

    go-zero_第7张图片

  7. 编写order业务逻辑

    func (l *GetOrderLogic) GetOrder(req *types.OrderReq) (resp *types.OrderReply, err error) {
    	u, err := l.svcCtx.UserRpc.GetUser(l.ctx, &user.IdRequest{
    		Id: "1",
    	})
    	if err != nil {
    		return nil, err
    	}
    
    	if u.Name != "generalzy" {
    		return nil, errors.New("用户不存在")
    	}
    
    	return &types.OrderReply{
    		Id:   req.Id,
    		Name: "test order",
    	}, nil
    }
    

    go-zero_第8张图片

  8. go build编译

启动

  1. 启动etcd:etcd.exe
    go-zero_第9张图片
  2. 启动user:user.exe -f ./etc/user.yaml
  3. 启动order:order.exe -f ./etc/order.yaml
  4. 访问url:http://localhost:8888/api/order/get/1返回:{"id":"1","name":"test order"}

goctl

goctl是go-zero微服务框架下的代码生成工具。使用 goctl 可显著提升开发效率,让开发人员将时间重点放在业务开发上,其功能有:

  1. api服务生成
  2. rpc服务生成
  3. model代码生成
  4. 模板管理

安装

go install github.com/zeromicro/go-zero/tools/goctl@latest

生成的api网关目录

.
├── etc
│   └── greet-api.yaml              // 配置文件
├── go.mod                          // mod文件
├── greet.api                       // api描述文件
├── greet.go                        // main函数入口
└── internal                        
    ├── config  
    │   └── config.go               // 配置声明type
    ├── handler                     // 路由及handler转发
    │   ├── greethandler.go
    │   └── routes.go
    ├── logic                       // 业务逻辑
    │   └── greetlogic.go
    ├── middleware                  // 中间件文件
    │   └── greetmiddleware.go
    ├── svc                         // logic所依赖的资源池
    │   └── servicecontext.go
    └── types                       // request、response的struct,根据api自动生成,不建议编辑
        └── types.go

生成的pb目录

.
├── etc
│   └── greet.yaml
├── go.mod
├── go.sum
├── greet // [1]
│   ├── greet.pb.go
│   └── greet_grpc.pb.go
├── greet.go
├── greet.proto
├── internal
│   ├── config
│   │   └── config.go
│   ├── logic
│   │   └── greetlogic.go
│   ├── server
│   │   └── streamgreeterserver.go
│   └── svc
│       └── servicecontext.go
└── streamgreeter
    └── streamgreeter.go

api语法

  • syntax语法声明
  • import语法块
  • info语法块
  • type语法块
  • service语法块
  • 隐藏通道

syntax

一个api语法文件只能有0或者1个syntax语法声明,如果没有syntax,则默认为v1版本

// api语法版本
syntax = "v1"

import语法块

import语法块可以导入拆分的api文件, 不同的api文件按照一定规则声明,可以降低阅读难度和维护难度。

import "foo.api"
import "foo/bar.api"

import(
    "bar.api"
    "foo/bar/foo.api"
)

info

info语法块是一个包含了多个键值对的语法体,其作用相当于一个api服务的描述

info(
    foo: "foo value"
    bar: "bar value"
    desc: "long long long long long long text"
)

type

在api服务中,需要用到一个结构体(类)来作为请求体,响应体的载体,因此需要声明一些结构体来完成这件事情, type语法块由golang的type演变而来,保留着一些golang type的特性,沿用golang特性有:

  1. 保留了golang内置数据类型bool,int,int8,int16,int32,int64,uint,uint8,uint16,uint32,uint64,uintptr ,float32,float64,complex64,complex128,string,byte,rune,
  2. 兼容golang struct风格声明
  3. 保留golang关键字
  4. 不支持time.Time数据类型,用int64表示,因为api支持客户端代码生成,并非所有客户端语言都有time.Time对应的类型
  5. 结构体名称、字段名称、不能为golang关键字
type Foo{
    Id int `path:"id"`
    Foo int `json:"foo"`
}

type Bar{
    Bar int `form:"bar"`
}

type(
    FooBar{
        FooBar int `json:"fooBar"`
    }
)

tag定义和golang中json tag语法一样,除了json tag外,go-zero还提供了另外一些tag来实现对字段的描述, 详情见下表。(tag修饰符需要在tag value后以英文逗号,隔开)

  • tag表

    绑定参数时,以下四个tag只能选择其中一个

    tag key 描述 提供方 有效范围 示例
    json json序列化tag golang request、response json:"fooo"
    path 路由path,如/foo/:id go-zero request path:"id"
    form 标志请求体是一个form(POST方法时)或者一个query(GET方法时/search?name=keyword) go-zero request form:"name"
    header HTTP header,如 Name: value go-zero request header:"name"
  • tag修饰符

    常见参数校验描述

    tag key 描述 提供方 有效范围 示例
    optional 定义当前字段为可选参数 go-zero request json:"name,optional"
    options 定义当前字段的枚举值,多个以竖线|隔开 go-zero request json:"gender,options=male"
    default 定义当前字段默认值 go-zero request json:"gender,default=male"
    range 定义当前字段数值范围 go-zero request json:"age,range=[0:120]"

service

service语法块用于定义api服务,包含服务名称,服务metadata,中间件声明,路由,handler等。

语法:

  1. serviceSpec:包含了一个可选语法块atServer和serviceApi语法块,其遵循序列模式(编写service必须要按照顺序,否则会解析出错)

  2. atServer: 可选语法块,定义key-value结构的server metadata,‘@server’ 表示这一个server语法块的开始,其可以用于描述serviceApi或者route语法块,其用于描述不同语法块时有一些特殊关键key 需要值得注意,见 atServer关键key描述说明。

  3. serviceApi:包含了1到多个serviceRoute语法块

  4. serviceRoute:按照序列模式包含了atDoc,handler和route

  5. atDoc:可选语法块,一个路由的key-value描述,其在解析后会传递到spec.Spec结构体,如果不关心传递到spec.Spec, 推荐用单行注释替代。

  6. handler:是对路由的handler层描述,可以通过atServer指定handler key来指定handler名称, 也可以直接用atHandler语法块来定义handler名称

  7. atHandler:‘@handler’ 固定token,后接一个遵循正则[a-zA-Z][a-zA-Z-]*)的值,用于声明一个handler名称

  8. route:路由,有httpMethod、path、可选request、可选response组成,httpMethod是必须是小写。

  9. body:api请求体语法定义,必须要由()包裹的可选的ID值

  10. replyBody:api响应体语法定义,必须由()包裹的struct、array(向前兼容处理,后续可能会废弃,强烈推荐以struct包裹,不要直接用array作为响应体)

  11. kvLit: 同info key-value

  12. serviceName: 可以有多个’-'join的ID值

  13. path:api请求路径,必须以’/‘或者’/:‘开头,切不能以’/‘结尾,中间可包含ID或者多个以’-'join的ID字符串

atServer关键key:

修饰service时

key 描述 示例
jwt 声明当前service下所有路由需要jwt鉴权,且会自动生成包含jwt逻辑的代码 jwt: Auth
group 声明当前service或者路由文件分组 group: login
middleware 声明当前service需要开启中间件 middleware: AuthMiddleware
prefix 添加路由分组 prefix: api

修饰route时

key 描述 示例
handler 声明一个handler -
@server(
  jwt: Auth
  group: foo
  middleware: AuthMiddleware
  prefix: api
)
service foo-api{
  @doc "foo"
  @handler foo
  post /foo/:id (Foo) returns (Bar)
}

service foo-api{
  @handler ping
  get /ping

  @doc "foo"
  @handler bar
  post /bar/:id (Foo)
}

注释

// doc
// comment

命令大全

api命令

goctl api是goctl中的核心模块之一,其可以通过.api文件一键快速生成一个api服务.

api命令说明:goctl api -h

NAME:
   goctl api - generate api related files

USAGE:
   goctl api command [command options] [arguments...]

COMMANDS:
   new       fast create api service
   format    format api files
   validate  validate api file
   doc       generate doc files
   go        generate go files for provided api in yaml file
   java      generate java files for provided api in api file
   ts        generate ts files for provided api in api file
   dart      generate dart files for provided api in api file
   kt        generate kotlin code for provided api file
   plugin    custom file generator

OPTIONS:
   -o value    the output api file
   --help, -h  show help

重点是 go子命令,其功能是生成golang api服务,通过goctl api go -h看一下使用帮助:

goctl api go -h

NAME:
   goctl api go - generate go files for provided api in yaml file

USAGE:
   goctl api go [command options] [arguments...]

OPTIONS:
   --dir value    the target dir
   --api value    the api file
   --style value  the file naming format, see [https://github.com/zeromicro/go-zero/tree/master/tools/goctl/config/readme.md]

//
--dir 代码输出目录
--api 指定api源文件
--style 指定生成代码文件的文件名称风格,详情见文件名称命名style说明

使用示例:

goctl api go -api user.api -dir . -style gozero

rpc命令

Goctl Rpc是goctl脚手架下的一个rpc服务代码生成模块,支持proto模板生成和rpc服务代码生成,通过此工具生成代码开发者只需要关注业务逻辑编写而不用去编写一些重复性的代码。

通过指定proto生成rpc服务:

  1. 生成proto模板

    goctl rpc template -o=user.proto
    
    syntax = "proto3";
    
    package user;
    option go_package="./user";
    
    message Request {
      string ping = 1;
    }
    
    message Response {
      string pong = 1;
    }
    
    service User {
      rpc Ping(Request) returns(Response);
    }
    
  2. 生成rpc服务代码:

    goctl rpc protoc user.proto --go_out=. --go-grpc_out=. --zrpc_out=.
    

rpc服务生成用法:

goctl rpc protoc -h

NAME:
   goctl rpc protoc - generate grpc code

USAGE:
   example: goctl rpc protoc xx.proto --go_out=./pb --go-grpc_out=./pb --zrpc_out=.

DESCRIPTION:
   for details, see https://go-zero.dev/cn/goctl-rpc.html

OPTIONS:
   --zrpc_out value  the zrpc output directory
   --style value     the file naming format, see [https://github.com/zeromicro/go-zero/tree/master/tools/goctl/config/readme.md]
   --home value      the goctl home path of the template
   --remote value    the remote git repo of the template, --home and --remote cannot be set at the same time, if they are, --remote has higher priority
                     The git repo directory must be consistent with the https://github.com/zeromicro/go-zero-template directory structure
   --branch value    the branch of the remote repo, it does work with --remote

参数说明:

  • –zrpc_out 可选,默认为proto文件所在目录,生成代码的目标目录
  • –style 可选,输出目录的文件命名风格,详情见https://github.com/zeromicro/go-zero/tree/master/tools/goctl/config/readme.md
  • –home 可选,指定模板路径
  • –remote 可选,指定模板远程仓库
  • –branch 可选,指定模板远程仓库分支,与 --remote 配合使用

可以理解为 zrpc 代码生成是用 goctl rpc $protoc_command --zrpc_out=${output} 模板,如原来生成 grpc 代码指令为

$ protoc user.proto --go_out=. --go-grpc_out=.

则生成 zrpc 代码指令就为

$ goctl rpc protoc user.proto --go_out=. --go-grpc_out=. --zrpc_out=.

proto import

错误的import:

syntax = "proto3";

package greet;

option go_package = "./greet";

import "base/common.proto";

message Request {
  string ping = 1;
}

message Response {
  string pong = 1;
}

service Greet {
  rpc Ping(base.In) returns(base.Out);// request和return 不支持import
}

正确import:

syntax = "proto3";

package greet;

option go_package =  "./greet";

import "base/common.proto";

message Request {
  base.In in = 1;// 支持import
}

message Response {
 base.Out out = 2;// 支持import
}

service Greet {
  rpc Ping(Request) returns(Response);
}

model命令

goctl model 为go-zero下的工具模块中的组件之一,目前支持识别mysql ddl进行model层代码生成,通过命令行或者idea插件(即将支持)可以有选择地生成带redis cache或者不带redis cache的代码逻辑。

如果使用GormMongoDB可以不适用model命令,框架过于大而全反而显得累赘啰嗦。

go-zero使用

配套目录介绍:

  1. models文件夹:存放所有数据库模型
  2. user文件夹:存放user相关微服务代码逻辑
  3. gateway文件夹:存放user api网关代码逻辑
    go-zero_第10张图片

集成mongodb

mongodb集成应该在微服务逻辑模块:

  1. user/internal/config下配置:

    package config
    
    import "github.com/zeromicro/go-zero/zrpc"
    
    type Config struct {
    	zrpc.RpcServerConf
    	// 总配置
    	MongoUri MongoUri
    }
    
    type MongoUri struct {
    	Uri string
    }
    
  2. 修改etc/user.yaml

    Name: user.rpc
    ListenOn: 0.0.0.0:8080
    Etcd:
      Hosts:
      - 127.0.0.1:2379
      Key: user.rpc
    MongoUri:
      Uri: mongodb://localhost:27017
    
  3. svc/servicecontext中初始化mongo client:

    package svc
    
    import (
    	"context"
    	"github.com/generalzy/zeropan/user/internal/config"
    	"go.mongodb.org/mongo-driver/mongo"
    	"go.mongodb.org/mongo-driver/mongo/options"
    )
    
    type ServiceContext struct {
    	Config      config.Config
    	MongoClient *mongo.Client
    }
    
    func NewServiceContext(c config.Config) *ServiceContext {
    	clientOptions := options.Client().ApplyURI(c.MongoUri.Uri)
    	clientOptions.SetMaxPoolSize(100)
    	client, _ := mongo.Connect(context.TODO(), clientOptions)
    	return &ServiceContext{
    		Config:      c,
    		MongoClient: client,
    	}
    }
    
  4. logic下对应实现部分编写业务逻辑代码,因为mongo client已经由配置——》初始化加入到了上下文中,所以可以l.svcCtx.MongoClient取到mongo client。

    package logic
    
    import (
    	"context"
    	"github.com/generalzy/zeropan/models"
    	"github.com/generalzy/zeropan/user/internal/svc"
    	"github.com/generalzy/zeropan/user/types/user"
    	"github.com/zeromicro/go-zero/core/logx"
    	"go.mongodb.org/mongo-driver/bson"
    	"go.mongodb.org/mongo-driver/bson/primitive"
    )
    
    type RetrieveUserLogic struct {
    	ctx    context.Context
    	svcCtx *svc.ServiceContext
    	logx.Logger
    }
    
    func NewRetrieveUserLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RetrieveUserLogic {
    	return &RetrieveUserLogic{
    		ctx:    ctx,
    		svcCtx: svcCtx,
    		Logger: logx.WithContext(ctx),
    	}
    }
    
    func (l *RetrieveUserLogic) RetrieveUser(in *user.RetrieveUserRequest) (*user.RetrieveUserResponse, error) {
    	objectID, err := primitive.ObjectIDFromHex(in.GetId())
    	if err != nil {
    		return nil, err
    	}
    
    	result := l.svcCtx.MongoClient.Database("zeropan").Collection("user").FindOne(
    		context.TODO(), bson.M{"_id": objectID})
    	if result.Err() != nil {
    		return nil, result.Err()
    	}
    
    	u := new(models.User)
    	if err := result.Decode(u); err != nil {
    		return nil, err
    	}
    	return &user.RetrieveUserResponse{
    		Id:       in.Id,
    		Name:     u.Name,
    		Email:    u.Email,
    		CreateAt: u.CreatedAt.Format("2006-01-02 15-04-05"),
    		UpdateAt: u.UpdatedAt.Format("2006-01-02 15-04-05"),
    	}, nil
    }
    
  5. 集成redis同理

集成jwt和中间件

go-zero已经集成了jwt库,所以直接在网关调用即可:

  1. 编写api网关文件:

    syntax = "v1"
    
    info(
    	title: "user svc gateway"
    	desc: "user svc gateway"
    	author: "generalzy"
    	email: "-"
    	version: "v1"
    )
    
    type RetrieveUserRequest {
    	ID string `json:"id"`
    }
    
    type RetrieveUserResponse {
    	Name     string `bson:"name" json:"name"`
    	Password string `bson:"password" json:"password"`
    	Email    string `bson:"email" json:"email"`
    
    	CreatedAt string `bson:"created_at" json:"createdAt"`
    	UpdatedAt string `bson:"updated_at" json:"updatedAt"`
    	DeletedAt string `bson:"deleted_at" json:"deletedAt"`
    }
    
    type CreateUserRequest {
    	Name     string `bson:"name" json:"name"`
    	Password string `bson:"password" json:"password"`
    	Email    string `bson:"email" json:"email""`
    }
    
    type CreateUserResponse {
    	ID        string `json:"id"`
    	Name      string `bson:"name" json:"name"`
    	Email     string `bson:"email" json:"email"`
    	CreatedAt string `bson:"created_at" json:"createdAt"`
    	UpdatedAt string `bson:"updated_at" json:"updatedAt"`
    }
    
    type UserLoginRequest {
    	Name     string `bson:"name" json:"name"`
    	Password string `bson:"password" json:"password"`
    }
    
    type UserLoginResponse {
    	Token string `bson:"token"`
    }
    
    @server (
    	// 写了jwt认证代表下边的接口需要jwt认证
    	jwt: Auth
    	middleware: AuthMiddleware
    )
    service user-api{
    	@handler CreateUserHandler
    	post /api/v1/user/create(CreateUserRequest)returns(CreateUserResponse)
    }
    
    service user-api{
    	@handler RetrieveUserHandler
    	get /api/v1/user/retrieve (RetrieveUserRequest) returns (RetrieveUserResponse)
    	@handler UserLoginHandler
    	post /api/v1/user/login(UserLoginRequest)returns (UserLoginResponse)
    }
    
  2. 使用命令生成代码:goctl api go -api user.api -dir . -style gozero

  3. 生成的目录如下:
    go-zero_第11张图片

  4. config配置Auth:

    package config
    
    import (
    	"github.com/generalzy/zeropan/gateway/internal/middleware"
    	"github.com/zeromicro/go-zero/rest"
    	"github.com/zeromicro/go-zero/zrpc"
    )
    
    type Config struct {
    	rest.RestConf
    	// 映射的是配置文件
    	UserRpc        zrpc.RpcClientConf
    	AuthMiddleware middleware.AuthMiddleware
    	// 校验
    	Auth Auth
    }
    
    type Auth struct {
    	AccessSecret string
    	AccessExpire int64
    }
    
    
  5. 修改yaml文件:

    Name: user-api
    Host: 0.0.0.0
    Port: 8888
    UserRpc:
      Etcd:
        Hosts:
          - 127.0.0.1:2379
        Key: user.rpc  
    Auth:
      AccessSecret: 31*&@(*&YUUYO*Q!PP()_@_@_+=
      AccessExpire: 36000000
    
  6. 观察routers,发现go-zero在接口上新增了一些关于jwt和middleware的东西:

    func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
    	server.AddRoutes(
    		rest.WithMiddlewares(
    			[]rest.Middleware{serverCtx.AuthMiddleware.Handle},
    			[]rest.Route{
    				{
    					Method:  http.MethodPost,
    					Path:    "/api/v1/user/create",
    					Handler: CreateUserHandler(serverCtx),
    				},
    			}...,
    		),
    		// 代表create接口需要认证
    		rest.WithJwt(serverCtx.Config.Auth.AccessSecret),
    	)
    
    	server.AddRoutes(
    		[]rest.Route{
    			{
    				Method:  http.MethodGet,
    				Path:    "/api/v1/user/retrieve",
    				Handler: RetrieveUserHandler(serverCtx),
    			},
    			{
    				Method:  http.MethodPost,
    				Path:    "/api/v1/user/login",
    				Handler: UserLoginHandler(serverCtx),
    			},
    		},
    	)
    }
    
  7. 在login接口中编写签发token的逻辑(原本应该在微服务中编写,然后在login api中写调用微服务的代码,此处图简便,直接写)

    func (l *UserLoginLogic) UserLogin(req *types.UserLoginRequest) (resp *types.UserLoginResponse, err error) {
    	// 简单生成token
    	claims := &jwt.RegisteredClaims{
    		ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Minute * 5)), // 过期时间
    		Issuer:    "generalzy",                                         // 签发人
    	}
    	// 生成token对象
    	token, err := jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString([]byte(l.svcCtx.Config.Auth.AccessSecret))
    	if err != nil {
    		return nil, err
    	}
    	return &types.UserLoginResponse{Token: token}, nil
    }
    
  8. 中间件需要到context上下文中初始化:

    import (
    	"github.com/generalzy/zeropan/gateway/internal/config"
    	"github.com/generalzy/zeropan/gateway/internal/middleware"
    	"github.com/generalzy/zeropan/user/userclient"
    	"github.com/zeromicro/go-zero/zrpc"
    )
    
    type ServiceContext struct {
    	Config config.Config
    	// 此处初始化配置
    	// UserRpc写的是userclient的User
    	// go-zero对client封装了一次
    	UserRpc userclient.User
    	// 中间件
    	AuthMiddleware middleware.AuthMiddleware
    }
    
    func NewServiceContext(c config.Config) *ServiceContext {
    	return &ServiceContext{
    		Config:         c,
    		UserRpc:        userclient.NewUser(zrpc.MustNewClient(c.UserRpc)),
    		AuthMiddleware: *middleware.NewAuthMiddleware(),
    	}
    }
    
  9. 具体逻辑需要到middleware中编写:

    type AuthMiddleware struct {
    }
    
    func NewAuthMiddleware() *AuthMiddleware {
    	return &AuthMiddleware{}
    }
    
    func (m *AuthMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc {
    	return func(w http.ResponseWriter, r *http.Request) {
    		// 前
    		next(w, r)
    		// 后
    	}
    }
    
  10. 登录测试:
    go-zero_第12张图片
    go-zero_第13张图片
    go-zero_第14张图片

  11. 如果是全局中间件,可以直接调用server.User():
    go-zero_第15张图片

由此可见,api网关也就是一个web框架,选择高度集成的框架,还是kong+grpc,或gin+grpc都可以完成微服务开发。

自定义错误

  1. 新建一个error类型:
    go-zero_第16张图片

    package errorx
    
    type ApiError struct {
    	Code int    `json:"code"`
    	Msg  string `json:"msg"`
    }
    
    func NewApiError(code int, msg string) *ApiError {
    	return &ApiError{
    		Code: code,
    		Msg:  msg,
    	}
    }
    
    // Error 实现error接口
    func (e *ApiError) Error() string {
    	return e.Msg
    }
    
    // ParamsError 110代表用户系统 1101代表错误码 微服务的错误可以体现系统
    var ParamsError = NewApiError(1101, "参数错误")
    
    type ApiErrorResponse struct {
    	Code int    `json:"code"`
    	Msg  string `json:"msg"`
    }
    
    // Data ApiErrorResponse打印详细的信息 否则.Error()会被调用只打印一个msg
    func (e *ApiError) Data() *ApiErrorResponse {
    	return &ApiErrorResponse{
    		Code: e.Code,
    		Msg:  e.Msg,
    	}
    }
    
  2. 使用paramError:

    func (l *RetrieveUserLogic) RetrieveUser(req *types.RetrieveUserRequest) (resp *types.RetrieveUserResponse, err error) {
    	userID := req.ID
    	if userID == "" {
    		return nil, errorx.ParamsError
    	}
    	// 获取用户信息
    	result, e := l.svcCtx.UserRpc.RetrieveUser(context.TODO(), &user.RetrieveUserRequest{
    		Id: userID,
    	})
    	if e != nil {
    		return nil, e
    	}
    
    	return &types.RetrieveUserResponse{
    		Name:      result.GetName(),
    		Password:  "",
    		Email:     result.GetEmail(),
    		CreatedAt: result.GetCreateAt(),
    		UpdatedAt: result.GetUpdateAt(),
    		DeletedAt: "",
    	}, nil
    }
    
  3. set自定义的error

    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)
    	// 设置自定义error
    	httpx.SetErrorHandlerCtx(func(ctx context.Context, err error) (int, any) {
    		switch e := err.(type) {
    		case *errorx.ApiError:
    			return http.StatusOK, e.Data()
    		default:
    			return http.StatusBadRequest, nil
    		}
    	})
    	httpx.SetErrorHandler(func(err error) (int, any) {
    		switch e := err.(type) {
    		case *errorx.ApiError:
    			return http.StatusOK, e.Data()
    		default:
    			return http.StatusBadRequest, nil
    		}
    	})
    	fmt.Printf("Starting server at %s:%d...\n", c.Host, c.Port)
    	server.Start()
    }
    
    
  4. 测试:
    go-zero_第17张图片

修改模板

如果想要自定义模板,可以执行:goctl template init,获取模板位置,通过修改模板实现自定制。
在这里插入图片描述
go-zero_第18张图片

集成zap日志

  1. 使用zap实现logx接口

    package zapx
    
    import (
    	"fmt"
    
    	"github.com/zeromicro/go-zero/core/logx"
    	"go.uber.org/zap"
    )
    
    const callerSkipOffset = 3
    
    type ZapWriter struct {
    	logger *zap.Logger
    }
    
    func NewZapWriter(opts ...zap.Option) (logx.Writer, error) {
    	opts = append(opts, zap.AddCallerSkip(callerSkipOffset))
    	logger, err := zap.NewProduction(opts...)
    	if err != nil {
    		return nil, err
    	}
    
    	return &ZapWriter{
    		logger: logger,
    	}, nil
    }
    
    func (w *ZapWriter) Alert(v interface{}) {
    	w.logger.Error(fmt.Sprint(v))
    }
    
    func (w *ZapWriter) Close() error {
    	return w.logger.Sync()
    }
    
    func (w *ZapWriter) Debug(v interface{}, fields ...logx.LogField) {
    	w.logger.Debug(fmt.Sprint(v), toZapFields(fields...)...)
    }
    
    func (w *ZapWriter) Error(v interface{}, fields ...logx.LogField) {
    	w.logger.Error(fmt.Sprint(v), toZapFields(fields...)...)
    }
    
    func (w *ZapWriter) Info(v interface{}, fields ...logx.LogField) {
    	w.logger.Info(fmt.Sprint(v), toZapFields(fields...)...)
    }
    
    func (w *ZapWriter) Severe(v interface{}) {
    	w.logger.Fatal(fmt.Sprint(v))
    }
    
    func (w *ZapWriter) Slow(v interface{}, fields ...logx.LogField) {
    	w.logger.Warn(fmt.Sprint(v), toZapFields(fields...)...)
    }
    
    func (w *ZapWriter) Stack(v interface{}) {
    	w.logger.Error(fmt.Sprint(v), zap.Stack("stack"))
    }
    
    func (w *ZapWriter) Stat(v interface{}, fields ...logx.LogField) {
    	w.logger.Info(fmt.Sprint(v), toZapFields(fields...)...)
    }
    
    func toZapFields(fields ...logx.LogField) []zap.Field {
    	zapFields := make([]zap.Field, 0, len(fields))
    	for _, f := range fields {
    		zapFields = append(zapFields, zap.Any(f.Key, f.Value))
    	}
    	return zapFields
    }
    
    
  2. 设置日志writer为zap:

    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)
    	// 设置自定义error
    	httpx.SetErrorHandlerCtx(func(ctx context.Context, err error) (int, any) {
    		switch e := err.(type) {
    		case *errorx.ApiError:
    			return http.StatusOK, e.Data()
    		default:
    			return http.StatusBadRequest, nil
    		}
    	})
    	httpx.SetErrorHandler(func(err error) (int, any) {
    		switch e := err.(type) {
    		case *errorx.ApiError:
    			return http.StatusOK, e.Data()
    		default:
    			return http.StatusBadRequest, nil
    		}
    	})
    	// 替换日志
    	writer, err := zapx.NewZapWriter()
    	logx.Must(err)
    	logx.SetWriter(writer)
    
    	fmt.Printf("Starting server at %s:%d...\n", c.Host, c.Port)
    	server.Start()
    }
    
  3. 测试:
    在这里插入图片描述

集成prometheus

windows下载链接:https://prometheus.io/download/

  1. 编辑prometheus的配置文件prometheus.yml,添加如下配置,并创建targets.json

    # my global config
    global:
      scrape_interval: 15s # Set the scrape interval to every 15 seconds. Default is every 1 minute.
      evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute.
      # scrape_timeout is set to the global default (10s).
    
    # Alertmanager configuration
    alerting:
      alertmanagers:
        - static_configs:
            - targets:
              # - alertmanager:9093
    
    # Load rules once and periodically evaluate them according to the global 'evaluation_interval'.
    rule_files:
      # - "first_rules.yml"
      # - "second_rules.yml"
    
    # A scrape configuration containing exactly one endpoint to scrape:
    # Here it's Prometheus itself.
    scrape_configs:
      # The job name is added as a label `job=` to any timeseries scraped from this config.
      # - job_name: "prometheus"
        # metrics_path defaults to '/metrics'
        # scheme defaults to 'http'.
        # static_configs:
        #   - targets: ["localhost:9090"]
        # job_name可以随便写
      - job_name: "user-rpc"
        file_sd_configs:
          - files:
              - targets.json
    
  2. targets.json

    [
        {
            "targets": ["127.0.0.1:9091"],
            "labels": {
                "job": "user-rpc",
                "app": "user-rpc",
                "env": "test",
                "instance": "127.0.0.1:8888"
            }
        }
    ]
    
  3. 在网关配置:

    Name: user-api
    Host: 0.0.0.0
    Port: 8888
    UserRpc:
      Etcd:
        Hosts:
          - 127.0.0.1:2379
        Key: user.rpc  
    Auth:
      AccessSecret: 31*&@(*&YUUYO*Q!PP()_@_@_+=
      AccessExpire: 36000000
    Prometheus:
      Host: 127.0.0.1
      Port: 9091
      Path: /metrics
    
  4. 启动:prometheus --config.file=prometheus.yml并测试:
    go-zero_第19张图片
    go-zero_第20张图片
    go-zero_第21张图片
    go-zero_第22张图片

分布式链路追踪——Jaeger

  1. docker安装docker run -di --rm --name jaeger -p6831:6831/udp -p16686:16686 -p6832:6832/udp -p5778:5778 -p4317:4317 -p4318:4318 -p14250:14250 -p14268:14268 -p14269:14269 -p9411:9411 jaegertracing/all-in-one:latest

  2. 访问jaeger
    go-zero_第23张图片

  3. go-zero自己做了链路追踪,但jaeger比较知名

  4. 网关处配置jaeger:

    Name: user-api
    Host: 0.0.0.0
    Port: 8888
    UserRpc:
      Etcd:
        Hosts:
          - 127.0.0.1:2379
        Key: user.rpc  
    Auth:
      AccessSecret: 31*&@(*&YUUYO*Q!PP()_@_@_+=
      AccessExpire: 36000000
    Prometheus:
      Host: 127.0.0.1
      Port: 9091
      Path: /metrics
    Telemetry:
      Name: user-rpc
      Endpoint: http://ip:14268/api/traces
      Sample: 1.0
      Batcher: jaeger
    
  5. 重启网关,请求几次api,然后访问http://8.130.45.145:16686/search?
    go-zero_第24张图片

分布式事务(DTM)

go-zero与dtm强强联合,推出了在go-zero中无缝接入dtm的极简方案,是go生态中首家提供分布式事务能力的微服务框架。该方案具备以下特征:

  1. dtm服务可以通过配置,直接注册到go-zero的注册中心
  2. go-zero能够以内建的target格式访问dtm服务器
  3. dtm能够识别go-zero的target格式,动态访问go-zero中的服务

原则上能不用就不用

分布式事务是在分布式系统中实现一致性的一种机制。在分布式系统中,由于各个节点之间的通信和数据交换可能存在延迟、故障、网络分区等问题,因此需要采用一些特殊的机制来保证数据的一致性。

然而,分布式事务的实现一般需要较高的复杂度和成本,例如需要引入分布式锁、两阶段提交等机制,同时也会对系统的性能产生一定的影响。因此,如果可以通过其他方式来保证系统的一致性,就可以避免使用分布式事务,从而提高系统的性能和可靠性。

在实际的系统设计中,可以采用以下方式来避免使用分布式事务:

  1. 尽量采用无状态的设计方式:无状态的设计方式可以使系统更容易扩展和维护,从而降低使用分布式事务的需求。

  2. 将系统分解成更小的服务:将系统分解成更小的服务可以降低服务之间的依赖关系,从而减少使用分布式事务的需求。

  3. 异步消息传递:将数据的传递方式改为异步消息传递可以避免数据的实时性问题,从而降低使用分布式事务的需求。

  4. 最终一致性:使用最终一致性的方式来处理数据更新可以降低分布式事务的需求。

需要注意的是,并不是所有的场景都适合避免使用分布式事务,一些需要强一致性保证的场景仍然需要使用分布式事务来保证数据的一致性。因此,在具体的系统设计中,需要根据实际的需求和场景来进行权衡和选择。

更多应用场景

在微服务架构中,当我们需要跨服务保证数据一致性时,原先的数据库事务力不从心,无法将跨库、跨服务的多个操作放在一个事务中。这样的应用场景非常多,我们可以列举出很多:

  1. 订单系统:需要保证创建订单和扣减库存要么同时成功,要么同时回滚
  2. 跨行转账场景:数据不在一个数据库,但需要保证余额扣减和余额增加要么同时成功,要么同时失败
  3. 积分兑换场景:需要保证积分扣减和权益增加同时成功,或者同时失败
  4. 出行订票场景:需要在第三方系统同时定几张票,要么同时成功,要么全部取消

面对这些本地事务无法解决的场景,我们需要分布式事务的解决方案,保证跨服务、跨数据库更新数据的一致性。

dtm不仅可以解决上述的分布式事务场景,还可以解决更多的与数据一致性相关的场景,包括:

  1. 数据库与缓存一致性: dtm 的二阶段消息,能够保证数据库更新操作,和缓存更新/删除操作的原子性
  2. 秒杀系统: dtm 能够保证秒杀场景下,创建的订单量与库存扣减数量完全一样,无需后续的人工校准
  3. 多种存储组合: dtm 已支持数据库、Redis、Mongo等多种存储,可以将它们组合为一个全局事务,保证数据的一致性

使用dtm

  1. 场景,api访问user用户服务,返回之后,再次访问score积分服务,即注册完成后给用户增加积分。如果积分增加出错,则用户注册也必须失败。按照正常逻辑是无法完成的,所以需要引入分布式事务。
    go-zero_第25张图片

  2. 下载dtm源码:https://github.com/dtm-labs/dtm

  3. 复制conf.sample.yml->conf.yml,并且打开go-zero注释:
    go-zero_第26张图片

  4. 编译dtm,然后启动:dtm -c conf.yml
    在这里插入图片描述

  5. 建库建表:

    create table if not EXISTS dtm_barrier.barrier(
    	id BIGINT(22) PRIMARY KEY AUTO_INCREMENT,
    	trans_type varchar(45) DEFAULT "",
    	gid VARCHAR(128) default "",
    	branch_id VARCHAR(128) default "",
    	op varchar(45) default "",
    	barrier_id varchar(45) DEFAULT "",
        reason varchar(45) default "" comment "the branch type who insert this record",
    	create_time datetime default now(),
    	update_time datetime default now(),
    	key(create_time),
    	key(update_time),
    	UNIQUE KEY(gid,branch_id,op,barrier_id)
    	) ENGINE = INNODB DEFAULT CHARSET =utf8mb4;
    
  6. 创建积分服务goctl rpc protoc pb/score.proto --go_out=./types --go-grpc_out=./types --zrpc_out=.

    syntax = "proto3";
    
    package score;
    
    option go_package = "./score";
    
    message UserScoreRequest{
      string id = 1;
      int32  score = 2;
    }
    
    message UserScoreResponse{
      string id = 1;
      int32 score = 2;
    }
    
    service User{
      rpc saveUserScore(UserScoreRequest)returns(UserScoreResponse);
      // 用作回滚回调
      rpc saveUserScoreCallback(UserScoreRequest)returns(UserScoreResponse);
    }
    
  7. ScoreRpc也注册到网关中:

    Name: user-api
    Host: 0.0.0.0
    Port: 8888
    UserRpc:
      Etcd:
        Hosts:
          - 127.0.0.1:2379
        Key: user.rpc
    ScoreRpc:
      Etcd:
        Hosts:
          - 127.0.0.1:2379
        Key: score.rpc
    Auth:
      AccessSecret: 31*&@(*&YUUYO*Q!PP()_@_@_+=
      AccessExpire: 36000000
    Prometheus:
      Host: 127.0.0.1
      Port: 9091
      Path: /metrics
    Telemetry:
      Name: user-rpc
      Endpoint: http://8.130.45.145:14268/api/traces
      Sample: 1.0
      Batcher: jaeger
    
  8. 修改新增逻辑:

    func (l *CreateUserLogic) CreateUser(req *types.CreateUserRequest) (resp *types.CreateUserResponse, err error) {
    	response, err := l.svcCtx.UserRpc.CreateUser(context.TODO(), &user.CreateUserRequest{
    		Name:     req.Name,
    		Email:    req.Email,
    		CreateAt: "",
    		UpdateAt: "",
    	})
    	if err != nil {
    		return nil, err
    	}
    	
    	// 走到此处时,user已经创建,如果此处失败,则user还在,不符合我们的需求。
    	scoreResponse, err := l.svcCtx.ScoreRpc.SaveUserScore(context.TODO(), &score.UserScoreRequest{
    		Id:    response.Id,
    		Score: 0,
    	})
    	if err != nil {
    		return nil, err
    	}
    	return &types.CreateUserResponse{
    		ID:        scoreResponse.Id,
    		Name:      response.Name,
    		Email:     response.Email,
    		CreatedAt: response.CreateAt,
    		UpdatedAt: response.UpdateAt,
    	}, nil
    }
    
  9. 测试创建user(我在score逻辑处将ID拼接了一个100)通过:
    go-zero_第27张图片

  10. 在create login处引入dtm:go get github.com/dtm-labs/dtm

    package logic
    
    import (
    	"context"
    	"github.com/generalzy/zeropan/score/types/score"
    	"github.com/generalzy/zeropan/user/types/user"
    
    	"github.com/generalzy/zeropan/gateway/internal/svc"
    	"github.com/generalzy/zeropan/gateway/internal/types"
    
    	_ "github.com/dtm-labs/dtmdriver-gozero"
    	"github.com/zeromicro/go-zero/core/logx"
    )
    
    var dtmServer = "etcd://localhost:2379/dtmservice"
    
  11. user的pb文件新增create callback:

    service User{
      rpc retrieveUser(RetrieveUserRequest)returns(RetrieveUserResponse);
      rpc createUser(CreateUserRequest)returns(CreateUserResponse);
      rpc deleteUser(DeleteUserRequest)returns(DeleteUserResponse);
      rpc createUserCallback(CreateUserRequest)returns(CreateUserResponse);
    }
    
  12. 使用dtm操作svc:

    func (l *CreateUserLogic) CreateUser(req *types.CreateUserRequest) (resp *types.CreateUserResponse, err error) {
    	gid := dtmgrpc.MustGenGid(dtmServer)
    	sagaGrpc := dtmgrpc.NewSagaGrpc(dtmServer, gid)
    	userServer, err := l.svcCtx.Config.UserRpc.BuildTarget()
    	if err != nil {
    		return nil, err
    	}
    	scoreServer, err := l.svcCtx.Config.ScoreRpc.BuildTarget()
    	if err != nil {
    		return nil, err
    	}
    	userReq := &user.CreateUserRequest{
    		Name:     req.Name,
    		Email:    req.Email,
    		CreateAt: "",
    		UpdateAt: "",
    	}
    	// dtm添加user
    	sagaGrpc.Add(userServer+"/user.User/createUser", userServer+"/user.User/createUserCallback", userReq)
    	// dtm添加score
    	scoreReq := &score.UserScoreRequest{
    		// Id可以另写一个获取ID的服务
    		Id:    "0x12231",
    		Score: 0,
    	}
    	sagaGrpc.Add(scoreServer+"/score.UserScore/saveUserScore", scoreServer+"/score.UserScore/saveUserScoreCallback", scoreReq)
    	sagaGrpc.WaitResult = true
    	err = sagaGrpc.Submit()
    	if err != nil {
    		return nil, err
    	}
    	//response, err := l.svcCtx.UserRpc.CreateUser(context.TODO(), &user.CreateUserRequest{
    	//	Name:     req.Name,
    	//	Email:    req.Email,
    	//	CreateAt: "",
    	//	UpdateAt: "",
    	//})
    	//if err != nil {
    	//	return nil, err
    	//}
    	//
    	//scoreResponse, err := l.svcCtx.ScoreRpc.SaveUserScore(context.TODO(), &score.UserScoreRequest{
    	//	Id:    response.Id,
    	//	Score: 0,
    	//})
    	//if err != nil {
    	//	return nil, err
    	//}
    	return &types.CreateUserResponse{
    		ID:        scoreReq.Id,
    		Name:      userReq.Name,
    		Email:     userReq.Email,
    		CreatedAt: userReq.CreateAt,
    		UpdatedAt: userReq.UpdateAt,
    	}, nil
    }
    
  13. 测试,新增成功:
    go-zero_第28张图片

    go-zero_第29张图片

  14. 遇到cannot use endpoint (variable of type func() string) as type string in array or slice literal 错误直接修改源码dsn := strings.Join([]string{schemeName + ":/", url.Authority, url.Endpoint()}, "/")

  15. 增加事务,需要把逻辑写入到事务中,其他同理,DB需要在上下文中加入

    func (l *SaveUserScoreLogic) SaveUserScore(in *score.UserScoreRequest) (*score.UserScoreResponse, error) {
    	barrier, err := dtmgrpc.BarrierFromGrpc(l.ctx)
    	if err != nil {
    		// 重试
    		return nil, status.Error(codes.Internal, err.Error())
    	}
    	err = barrier.CallWithDB(l.svcCtx.DB, func(tx *sql.Tx) error {
    		// 此处需要用tx或其他链接写实现saveScore的事务逻辑
    		logx.Info("saveUserScore called>>>>>>>>>>>>>")
    		return nil
    	})
    	if err != nil {
    		// 回滚状态
    		return nil, status.Error(codes.Aborted, err.Error())
    	}
    	return &score.UserScoreResponse{
    		Id:    in.Id + "100",
    		Score: in.Score + 100,
    	}, nil
    }
    
    
  16. 测试正常逻辑:
    go-zero_第30张图片
    go-zero_第31张图片

  17. 手动报错:

    func (l *SaveUserScoreLogic) SaveUserScore(in *score.UserScoreRequest) (*score.UserScoreResponse, error) {
    	barrier, err := dtmgrpc.BarrierFromGrpc(l.ctx)
    	if err != nil {
    		// 重试
    		return nil, status.Error(codes.Internal, err.Error())
    	}
    	err = barrier.CallWithDB(l.svcCtx.DB, func(tx *sql.Tx) error {
    		// 此处需要用tx或其他链接写实现saveScore的事务逻辑
    		logx.Info("saveUserScore called>>>>>>>>>>>>>")
    		return errors.New("this is a error")
    	})
    	if err != nil {
    		// 回滚状态
    		return nil, status.Error(codes.Aborted, err.Error())
    	}
    	return &score.UserScoreResponse{
    		Id:    in.Id + "100",
    		Score: in.Score + 100,
    	}, nil
    }
    

    在这里插入图片描述

代码存储

本demo代码存储:https://github.com/Generalzy/go-zero-demo

你可能感兴趣的:(微服务,golang,java,分布式)