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中的目录
- 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"
- 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,
},
}
}
- 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)
}
}
}
- 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
}
- types: api生成的文件,和pb.go一样,不用管,里面生成了你在api中写的那些结构体,请求响应什么的
rpc服务的文件同理
model是通过goctl和sql脚本一起生成的使用go-zero的orm框架的model层,应该还是好理解的
感觉go-zero还是很好用的