go-zero入门 | 青训营笔记

259 阅读4分钟

go-zero设计理念

对于微服务框架的设计,我们期望保障微服务稳定性的同时,也要特别注重研发效率。所以设计之初,我们就有如下一些准则:

  • 保持简单,第一原则
  • 弹性设计,面向故障编程
  • 工具大于约定和文档
  • 高可用
  • 高并发
  • 易扩展
  • 对业务开发友好,封装复杂度
  • 约束做一件事只有一种方式

go-zero特性

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

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

如下图,我们从多个层面保障了整体服务的高可用:

⭐目录结构

如果第一次接触 go-zero,可以看看下面的介绍,当然,需要你自己进入编辑器去熟悉

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

go-zero至少需要两个部分:go.mod,service(app)。

前者不用多说了吧,service肯定就是来放每个微服务的代码的。每个服务的目录下还有api、rpc、model,这三个是最基本的,api就是对外暴露的api服务,rpc就是不同服务相互调用的服务,model是和数据库和数据操作相关的。

然后

再看看我下面的这个例子

先说明,不管是api还是rpc,里面都是etc、internal、xxx.api 或者 xxx.proto、xxx.go。

  • api服务就是 xxx.api,rpc服务是 xxx.proto。
  • xxx.go 是main函数所在文件。
  • etc放配置文件,默认yam格式

api 文件

此处不做介绍

但要知道我们依靠这个来生成代码

xxx.go 入口文件

第一行就通过命令行读取了我们的配置文件,就和

go run xxx.go -f service/cmdty/cmd/api/etc/cmdty-api.yaml

一样

然后,看到,mustload加载了配置文件,然后通过基本的配置获取go-zero封装的server

继续,我们new了一个 服务上下文,把handler注册到server中,并把ctx传进去,然后就可以开启服务了

var configFile = flag.String("f", "service/cmdty/cmd/api/etc/cmdty-api.yaml", "the config file")

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)

	mq.InitRabbitMQ(ctx)

	fmt.Printf("Starting server at %s:%d...\n", c.Host, c.Port)
	server.Start()
}

internal中的目录

  1. config目录:包含一个config.go,很明显是读取配置文件的,比如:
type Config struct {
    rest.RestConf

    Mysql struct {
        Dsn string
    }

    Redis struct {
        Addr     string
        Password string
        Db       int
    }

    RabbitMQ utils.RabbitMQConf
}

那我们刚刚介绍的etc的yaml中就应该是

Name: cmdty-api
Host: 0.0.0.0
Port: 12350

Mysql:
  dsn: "root:123456@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"

Redis:
  Addr: "127.0.0.1:6379"
  Password: ""
  Db: 0

Rabbitmq:
  RmqUrl: "amqp://admin:123456@127.0.0.1:5672"
  1. svc目录:serviceContext上下文,肯定是用来存储并传递一些东西的

看看我写的案例

可以看到 ServiceContext 这个结构体中有一个刚刚的config,这也是通过goctl生成的代码中带有的,是基本配置,因为结构体中的一些成员也是通过config中的值来进行初始化的。那么在这个上下文中初始化后,就可以供其他地方使用了

type ServiceContext struct {
    Config config.Config

    // mysql
    CmdtyInfo    model.CmdtyInfoModel
    CmdtyCollect model.CmdtyCollectModel
    CmdtyTag     model.CmdtyTagModel

    // redis
    RedisClient *redis.Client

    // rabbitMQ
    RmqCore *utils.RabbitmqCore
}

func NewServiceContext(c config.Config) *ServiceContext {
    c1 := sqlx.NewMysql(c.Mysql.Dsn)
    c2, err := amqp.Dial(c.RabbitMQ.RmqUrl)
    if err != nil {
        panic("[RABBITMQ ERROR] NewServiceContext 连接不到rabbitmq")
    }
    channel, err := c2.Channel()
    if err != nil {
        panic("[RABBITMQ ERROR] NewServiceContext 获取rabbitmq通道失败")
    }
    return &ServiceContext{
        Config:       c,
        CmdtyInfo:    model.NewCmdtyInfoModel(c1),
        CmdtyCollect: model.NewCmdtyCollectModel(c1),
        CmdtyTag:     model.NewCmdtyTagModel(c1),
        RedisClient: redis.NewClient(&redis.Options{
            Addr:     c.Redis.Addr,
            Password: c.Redis.Password,
            DB:       c.Redis.Db,
        }),
        RmqCore: &utils.RabbitmqCore{
            Conn:    c2,
            Channel: channel,
        },
    }
}
  1. handler目录:路由注册

这是一段官方案例的代码,可以看到在每个路由中,我们会传进上下文,所以我们就可以在handler中去使用刚刚上下文的东西了

func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
    server.AddRoutes(
        []rest.Route{
            {
                Method:  http.MethodGet,
                Path:    "/api/order/get/:id",
                Handler: getOrderHandler(serverCtx),
            },
        },
    )
}

相应的handler中,我们可以进行参数的处理,然后再去交给logic(service层)去处理

func getOrderHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		var req types.OrderReq
		if err := httpx.Parse(r, &req); err != nil {
			httpx.ErrorCtx(r.Context(), w, err)
			return
		}
		l := logic.NewGetOrderLogic(r.Context(), svcCtx)
		resp, err := l.GetOrder(&req)
		if err != nil {
			httpx.ErrorCtx(r.Context(), w, err)
		} else {
			httpx.OkJsonCtx(r.Context(), w, resp)
		}
	}
}
  1. logic目录:真正处理逻辑的地方

刚刚的handler中,new了一个这里的logic的结构体,可能感觉有点多余,但还是为了使用我们的上下文,其实很方便的,一开始确实看得有点懵

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.OrderReq) (resp *types.OrderReply, err error) {
	user, err := l.svcCtx.UserRpc.GetUser(l.ctx, &user.IdRequest{
		Id: "1",
	})
	if err != nil {
		return nil, err
	}
	if user.Name != "小胖" {
		return nil, errors.New("用户不存在")
	}

	return &types.OrderReply{
		Id:   req.Id,
		Name: "test order",
	}, nil
}
  1. types: api生成的文件,和pb.go一样,不用管,里面生成了你在api中写的那些结构体,请求响应什么的

rpc服务的文件同理

model是通过goctl和sql脚本一起生成的使用go-zero的orm框架的model层,应该还是好理解的

感觉go-zero还是很好用的