Go业务框架从Gin-Api到Kratos

golang cyanprobe 3年前 (2022-04-21) 3017次浏览 已收录 0个评论

前言

之前业务侧从node过度到go, 由于基本的业务逻辑由业务中台来承载, 复杂度不算高, 包括公司内SRE的管理基建模块也是用gin搭建的, 内部使用是没问题,但对外的复杂的业务逻辑对于gin的封装还远远不够。

Gin

第一版用的是 https://github.com/xinliangnote/go-gin-api 这个项目对于gin的封装主要拿来改了context 和 core 部分, go-gin-api对gin的封装很简单,借由gin的中间件利用sync.pool重新封装上下文,并在新的context上绑定paylod或者token等key标识,logger等方法,并提供响应的方法,方便后续开发,并在全局使用recover,和错误处理中间件。

if err != nil {
   ctx.AbortWithError(err)
   return
}
ctx.Payload(d)

可以达成上面的效果, 其实是挂载error到上下文,然后在gin中间件读取key统一处理错误。

Gin所面临的问题

在pay项目中使用感触就是上下文的传递太麻烦, mvc那一套必须传递上下文,不同service 必须 new service对象来实现内聚。

api := r.mux.Group("/api", core.WrapAuthHandler(r.interceptors.CheckLogin), r.interceptors.CheckSignature(), r.interceptors.CheckRBAC())
{
// authorized
authorizedHandler := authorized.New(r.logger, r.db, r.cache)
api.POST("/authorized", authorizedHandler.Create())
api.GET("/authorized", authorizedHandler.List())
api.PATCH("/authorized/used", authorizedHandler.UpdateUsed())
api.DELETE("/authorized/:id", core.AliasForRecordMetrics("/api/authorized/info"), authorizedHandler.Delete())
}

在router层来构建路handler(各service对象) 如: authorized.New(r.logger, r.db, r.cache) , 这样能更有效的实现service的功能,不用重复的初始化service,或者依托方法上下文传递。但这样做还有问题, 还是避免不了由下层依赖服务的层层初始化, 且各种依赖gin来实现的项目各不相同,没有相同的约定会导致代码越写越花, 依托于包装gin context扩展性将会变的很差。

Kratos

官网:  https://go-kratos.dev/

Kratos是B站后端框架的开源版本。

官网 Principles

  • 简单:不过度设计,代码平实简单;
  • 通用:通用业务开发所需要的基础库的功能;
  • 高效:提高业务迭代的效率;
  • 稳定:基础库可测试性高,覆盖率高,有线上实践安全可靠;
  • 健壮:通过良好的基础库设计,减少错用;
  • 高性能:性能高,但不特定为了性能做 hack 优化,引入 unsafe ;
  • 扩展性:良好的接口设计,来扩展实现,或者通过新增基础库目录来扩展功能;
  • 容错性:为失败设计,大量引入对 SRE 的理解,鲁棒性高;
  • 工具链:包含大量工具链,比如 cache 代码生成,lint 工具等等;

使用感受:

kratos抽象出transport层, 下层为http(基于mux路由实现) 或grpc, http header 和grpc metadata 统一抽象为metadata, 接口层面采用protobuf进行定义,并使用https://github.com/lazada/protoc-gen-go-http 作为 protoc-gen插件生产http的pb文件。

syntax = "proto3";

package fd_biz_service.v1;

import "google/api/annotations.proto";
import "protoc-gen-openapiv2/options/annotations.proto";

option go_package = "git.gaoding.com/gaoding/fd-biz-service/api/v1;v1";
option java_multiple_files = true;
option java_package = "dev.kratos.api.fd-biz-service.v1";
option java_outer_classname = "FD_BIZ_SERVICE_V1";

option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = {
  info: {
    title: "Helloworld examples";
    version: "1.0";
    contact: {
      name: "gRPC-Gateway project";
      url: "https://github.com/grpc-ecosystem/grpc-gateway";
      email: "none@example.com";
    };
    license: {
      name: "BSD 3-Clause License";
      url: "https://github.com/grpc-ecosystem/grpc-gateway/blob/master/LICENSE.txt";
    };
    extensions: {
      key: "x-something-something";
      value {
        string_value: "yadda";
      }
    }
  };
};

// The greeting service definition.
service User {
  // Sends a greeting
  rpc TestUser (UserRequest) returns (UserReply)  {
    option (google.api.http) = {
      get: "/user/{name}"
    };
    option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
      summary: "Get a message.";
      operation_id: "getMessage";
      tags: "echo";
      responses: {
        key: "200"
        value: {
          description: "OK";
        }
      }
    };
  }

  rpc GetUserByToken (UserRequest) returns (UserReply)  {
    option (google.api.http) = {
      get: "/user/token"
    };
    option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
      summary: "Get a message.";
      operation_id: "getMessage";
      tags: "echo";
      responses: {
        key: "200"
        value: {
          description: "OK";
        }
      }
    };
  }
}

// The request message containing the user's name.
message UserRequest {
  option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = {

  };
  string name = 1;
}

// The response message containing the greetings
message UserReply {
      option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = {
        json_schema: {
          title: "SimpleMessage"
          description: "A simple message."
          required: ["id"]
        }
      };

      // Id represents the message identifier.
      string id = 1 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
            description: "The unique identifier of the simple message."
          }];
}

上面的代码简单演示了定义一个user接口和swagger的定义,参数验证方面可以使用https://github.com/envoyproxy/protoc-gen-validate 实现

定义完后执行

api:
   protoc --proto_path=. \
          --proto_path=./third_party \
          --go_out=paths=source_relative:. \
          --go-http_out=paths=source_relative:. \
          --go-grpc_out=paths=source_relative:. \
          --openapi_out=. \
          $(API_PROTO_FILES)

可生成api pb文件, 内容如下

// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
//     protoc-gen-go v1.27.1
//     protoc        v3.20.0--rc1
// source: api/fd_biz_service/v1/user.proto

package v1

import (
   _ "github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2/options"
   _ "google.golang.org/genproto/googleapis/api/annotations"
   protoreflect "google.golang.org/protobuf/reflect/protoreflect"
   protoimpl "google.golang.org/protobuf/runtime/protoimpl"
   reflect "reflect"
   sync "sync"
)

const (
   // Verify that this generated code is sufficiently up-to-date.
   _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
   // Verify that runtime/protoimpl is sufficiently up-to-date.
   _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)

// The request message containing the user's name.
type UserRequest struct {
   state         protoimpl.MessageState
   sizeCache     protoimpl.SizeCache
   unknownFields protoimpl.UnknownFields

   Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
}

func (x *UserRequest) Reset() {
   *x = UserRequest{}
   if protoimpl.UnsafeEnabled {
      mi := &file_api_fd_biz_service_v1_user_proto_msgTypes[0]
      ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
      ms.StoreMessageInfo(mi)
   }
}

func (x *UserRequest) String() string {
   return protoimpl.X.MessageStringOf(x)
}

func (*UserRequest) ProtoMessage() {}

func (x *UserRequest) ProtoReflect() protoreflect.Message {
   mi := &file_api_fd_biz_service_v1_user_proto_msgTypes[0]
   if protoimpl.UnsafeEnabled && x != nil {
      ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
      if ms.LoadMessageInfo() == nil {
         ms.StoreMessageInfo(mi)
      }
      return ms
   }
   return mi.MessageOf(x)
}

// Deprecated: Use UserRequest.ProtoReflect.Descriptor instead.
func (*UserRequest) Descriptor() ([]byte, []int) {
   return file_api_fd_biz_service_v1_user_proto_rawDescGZIP(), []int{0}
}

func (x *UserRequest) GetName() string {
   if x != nil {
      return x.Name
   }
   return ""
}

// The response message containing the greetings
type UserReply struct {
   state         protoimpl.MessageState
   sizeCache     protoimpl.SizeCache
   unknownFields protoimpl.UnknownFields

   // Id represents the message identifier.
   Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
}

func (x *UserReply) Reset() {
   *x = UserReply{}
   if protoimpl.UnsafeEnabled {
      mi := &file_api_fd_biz_service_v1_user_proto_msgTypes[1]
      ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
      ms.StoreMessageInfo(mi)
   }
}

func (x *UserReply) String() string {
   return protoimpl.X.MessageStringOf(x)
}

func (*UserReply) ProtoMessage() {}

func (x *UserReply) ProtoReflect() protoreflect.Message {
   mi := &file_api_fd_biz_service_v1_user_proto_msgTypes[1]
   if protoimpl.UnsafeEnabled && x != nil {
      ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
      if ms.LoadMessageInfo() == nil {
         ms.StoreMessageInfo(mi)
      }
      return ms
   }
   return mi.MessageOf(x)
}

// Deprecated: Use UserReply.ProtoReflect.Descriptor instead.
func (*UserReply) Descriptor() ([]byte, []int) {
   return file_api_fd_biz_service_v1_user_proto_rawDescGZIP(), []int{1}
}

func (x *UserReply) GetId() string {
   if x != nil {
      return x.Id
   }
   return ""
}

var File_api_fd_biz_service_v1_user_proto protoreflect.FileDescriptor

var file_api_fd_biz_service_v1_user_proto_rawDesc = []byte{
   0x0a, 0x20, 0x61, 0x70, 0x69, 0x2f, 0x66, 0x64, 0x5f, 0x62, 0x69, 0x7a, 0x5f, 0x73, 0x65, 0x72,

}

var (
   file_api_fd_biz_service_v1_user_proto_rawDescOnce sync.Once
   file_api_fd_biz_service_v1_user_proto_rawDescData = file_api_fd_biz_service_v1_user_proto_rawDesc
)

func file_api_fd_biz_service_v1_user_proto_rawDescGZIP() []byte {
   file_api_fd_biz_service_v1_user_proto_rawDescOnce.Do(func() {
      file_api_fd_biz_service_v1_user_proto_rawDescData = protoimpl.X.CompressGZIP(file_api_fd_biz_service_v1_user_proto_rawDescData)
   })
   return file_api_fd_biz_service_v1_user_proto_rawDescData
}

var file_api_fd_biz_service_v1_user_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_api_fd_biz_service_v1_user_proto_goTypes = []interface{}{
   (*UserRequest)(nil), // 0: helloworld.v1.UserRequest
   (*UserReply)(nil),   // 1: helloworld.v1.UserReply
}
var file_api_fd_biz_service_v1_user_proto_depIdxs = []int32{
   0, // 0: helloworld.v1.User.TestUser:input_type -> helloworld.v1.UserRequest
   0, // 1: helloworld.v1.User.GetUserByToken:input_type -> helloworld.v1.UserRequest
   1, // 2: helloworld.v1.User.TestUser:output_type -> helloworld.v1.UserReply
   1, // 3: helloworld.v1.User.GetUserByToken:output_type -> helloworld.v1.UserReply
   2, // [2:4] is the sub-list for method output_type
   0, // [0:2] is the sub-list for method input_type
   0, // [0:0] is the sub-list for extension type_name
   0, // [0:0] is the sub-list for extension extendee
   0, // [0:0] is the sub-list for field type_name
}

func init() { file_api_fd_biz_service_v1_user_proto_init() }
func file_api_fd_biz_service_v1_user_proto_init() {
   if File_api_fd_biz_service_v1_user_proto != nil {
      return
   }
   if !protoimpl.UnsafeEnabled {
      file_api_fd_biz_service_v1_user_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
         switch v := v.(*UserRequest); i {
         case 0:
            return &v.state
         case 1:
            return &v.sizeCache
         case 2:
            return &v.unknownFields
         default:
            return nil
         }
      }
      file_api_fd_biz_service_v1_user_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
         switch v := v.(*UserReply); i {
         case 0:
            return &v.state
         case 1:
            return &v.sizeCache
         case 2:
            return &v.unknownFields
         default:
            return nil
         }
      }
   }
   type x struct{}
   out := protoimpl.TypeBuilder{
      File: protoimpl.DescBuilder{
         GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
         RawDescriptor: file_api_fd_biz_service_v1_user_proto_rawDesc,
         NumEnums:      0,
         NumMessages:   2,
         NumExtensions: 0,
         NumServices:   1,
      },
      GoTypes:           file_api_fd_biz_service_v1_user_proto_goTypes,
      DependencyIndexes: file_api_fd_biz_service_v1_user_proto_depIdxs,
      MessageInfos:      file_api_fd_biz_service_v1_user_proto_msgTypes,
   }.Build()
   File_api_fd_biz_service_v1_user_proto = out.File
   file_api_fd_biz_service_v1_user_proto_rawDesc = nil
   file_api_fd_biz_service_v1_user_proto_goTypes = nil
   file_api_fd_biz_service_v1_user_proto_depIdxs = nil
}

wire

kratos 利用wire来实现依赖注入,方便了许多,要对依赖关系很清晰, 入手需要一定的理解成本。

openapi

kratos直接生成的openapi是无法被导入到yapi中的,  线上直接看可以引入github.com/go-kratos/swagger-api/openapiv2,但是需要把一类接口定义写在一个protobuf文件中,很难修改和管理,推荐还是使用openapi_out protoc 的插件来生成文档,但此时是yaml的且不能在线导入,所以我写了个路由挂载上去转在线json,这样就能通过yapi的定时合并同步文档了。

配置

kratos的config可以支持render写法,可以吧env环境变量渲染到配置中,且支持默认配置, 由于kratos自带的env.NewSource()不能满足每次载入默认的本地配置, 可以先引入godotenv来进行环境变量文件的预载。

server:
  http:
    addr: 0.0.0.0:8000
    timeout: 1s
  grpc:
    addr: 0.0.0.0:9000
    timeout: 1s

config:
  data:
    database:
      driver: mysql
      source: root:root@tcp(127.0.0.1:3306)/test
    redis:
      redis_addr: "${REDIS_ADDR:localhost:6379}"
      redis_pass: "${REDIS_PASS:}"
      redis_db: "${REDIS_DB:0}"
      read_timeout: 0.2s
      write_timeout: 0.2s

微服务架构的样板

可参照: https://github.com/go-kratos/beer-shop

因为我这边涉及到的业务线与技术中台的交互比较多,与beer-shop不同的是,我希望抽离出一个中台的sdk,然后处理与中台的业务交互逻辑, 这部分是可复用的,多个业务线共同可调用的, 所以这边的服务是把platform和data层面的ptClient抽离出来作为一个共用子仓库(微服务多仓库模式),其他业务线都属于单独的子仓库引用公共仓库。像nodejs可以使用npm link 进行调试,对应的go可以选择mod replace 将公共包指向本地的子仓库。

多仓库避免了一些问题: 内部的DRMS平台只能保证一个服务对应一个仓库,或者服务编排的形式发布,这种是高内聚的,而我对应的是业务线之间的关联并不强,同时滚动发布是不科学的。另一方面,单仓库容易一个公共的代码的bug影响多个业务线,也无形加大了测试成本。业务线子仓库锁定公共仓库版本,就降低了这种风险。


CyanProbe , 版权所有丨如未注明 , 均为原创丨本网站采用BY-NC-SA协议进行授权
转载请注明原文链接:Go业务框架从Gin-Api到Kratos
喜欢 (57)
发表我的评论
取消评论

表情 贴图 加粗 删除线 居中 斜体 签到

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址