背景
(1) 部门之前的框架是一个自研的mvc框架,依赖的旧lib库很多,没有迭代更新了,有一些历史的bug,暂时没精力维护,为了能提高开发效率,内部发起了重新封装一个新框架的想法,目前已经在新项目使用,运行1年+,比较稳定,还在持续迭代更新中
(2) 调研了目前的已有框架, 后来选择了gin 作为基础框架进行二次开发
新框架介绍
(1) 采用go 1.17 , 比现有 1.13 性能好
- 提高了 defer 的性能,几乎是零开销
- 库的升级和性能优化
- ......
(2)新框架组成部分
路由(gin) 、redis、kafka、ucm配置、中间件、promethus监控、zap logger(日志滚动)、memcache、gorm 、graphql 等
(3) project layout
.
├── Makefile
├── README.md
├── app
│ ├── default
│ ├── example
├── cmd
│ └── main
├── config
│ ├── app_prod.yaml
│ └── app_test.yaml
├── docker
│ ├── Dockerfile
│ ├── docker-compose.yml
│ └── env
├── docs
├── go.mod
├── go.sum
├── library
├── routers
│ ├── monitor.go
│ └── routers.go
└── scripts
├── folder
└── mysql_tools
单个app下面的 pkg 目录结构
├── controller (CRSM分层) 控制层(c->r->s->m)
│ └── api
├── middleware
├── model DAO层
│ └── xxxModel.go
├── repository 业务聚合层(可不用)
│ └── echo_repository
├── router
│ └── router.go
├── service 业务逻辑(服务)层
│ └── echo_service.go
├── util
└── worker
(4) 路由
type Option func(*gin.Engine)
var options = []Option{}
// 注册app的路由配置
func Include(opts ...Option) {
options = append(options, opts...)
}
// 初始化
func Init() *gin.Engine {
// 加载多个APP的路由配置
//Include(example_router.Routers, Health, gql_router.GplRouters, default_router.Routers)
Include(Health, default_router.Routers)
r := gin.New()
for _, opt := range options {
opt(r)
}
r.SetTrustedProxies(nil)
return r
}
// 路由组
func Routers(engine *gin.Engine) {
// 外部api
apiRouters(engine)
}
// 外部api
func apiRouters(engine *gin.Engine) {
apiGroup := engine.Group("/go_preg_user/api", gin.Recovery(), middleware.IpMiddleware(), middleware.RequestIdMiddleware(), api_auth.ApiAuth(), middleware.FormatJsonMiddleware())
{
apiGroup.GET("/cookie/check_login", api.LoginController.CheckLoginAction)
}
}
gin 路由结构Radix Tree(压缩前缀数, 空间换时间,效率比hash 高,可以支持正则匹配)

在gin的路由中,每一个Http Method(GET, PUT, POST…)都对应了一棵 radix tree
func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
// ...
// 获取method对应的树,如果没有就创建
root := engine.trees.get(method)
if root == nil {
// 创建radix tree,只有根节点
root = new(node)
engine.trees = append(engine.trees, methodTree{method: method, root: root})
}
root.addRoute(path, handlers)
}
type node struct {
// 节点路径,比如上面的s,earch,和upport
path string
// 节点是否是参数节点,比如上面的:post
wildChild bool
// 节点类型,包括static, root, param, catchAll
// static: 静态节点,比如上面的s,earch等节点
// root: 树的根节点
// catchAll: 有*匹配的节点
// param: 参数节点
nType nodeType
// 路径上最大参数个数
maxParams uint8
// 和children字段对应, 保存的是分裂的分支的第一个字符
// 例如search和support, 那么s节点的indices对应的"eu"
// 代表有两个分支, 分支的首字母分别是e和u
indices string
// 儿子节点
children []*node
// 处理函数
handlers HandlersChain
// 优先级,子节点注册的handler数量
priority uint32
}
radix tree可以被认为是一棵简洁版的前缀树。拥有共同前缀的节点也共享同一个父节点。下面是一个GET方法对应的路由树的结构:
Priority Path Handle
9 \ *<1>
3 ├s nil
2 |├earch\ *<2>
1 |└upport\ *<3>
2 ├blog\ *<4>
1 | └:post nil
1 | └\ *<5>
2 ├about-us\ *<6>
1 | └team\ *<7>
1 └contact\ *<8>
*<num>是方法(handler)对应的指针,从根节点遍历到叶子节点我们就能得到完整的路由表,图中的示例实现了以下路由:
GET("/", func1)
GET("/search/", func2)
GET("/support/", func3)
GET("/blog/", func4)
GET("/blog/:post/", func5)
GET("/about-us/", func6)
GET("/about-us/team/", func7)
GET("/contact/", func8)
:post是真实的post name的一个占位符(就是一个参数)。这里体现了radix tree相较于hash-map的一个优点,树结构允许我们的路径中存在动态的部分(参数),因为我们匹配的是路由的模式而不是hash值
(5) 拦截器
可以对每个路由group 进行单独设置,按顺序进行执行
apiGroup := engine.Group("/go_user/api", gin.Recovery(), middleware.IpMiddleware(), middleware.RequestIdMiddleware(), api_auth.ApiAuth(), middleware.FormatJsonMiddleware())
(6) 控制器
type cookieController struct {
*controller.BaseController
}
var CookieController = new(cookieController)
// http://localhost:900/go_user/api/user/check_login
func (ctr cookieController) CheckLoginAction(ctx *gin.Context) {
params := &common.CheckLoginParams{}
if ctx.ShouldBind(params) != nil || (!params.Validate(ctx)) {
ctr.Fail(ctx, nil, "参数错误")
return
}
if user_repository.GetRepository().IsLogin(ctx, params) {
result := common.CheckLoginResponse{
xx: params.xx,
}
zap_logger.Infof(ctx, common.TAG_NAME, "check succ params:%+v", params)
ctr.Success(ctx, result)
} else {
zap_logger.Warnf(ctx, common.TAG_NAME, "check fail params:%+v", params)
ctr.Fail(ctx, nil, "不正确")
}
}
(7) model 对应表生成 struct 工具
go install github.com/Shelnutt2/db2struct/cmd/db2struct@latest
~/gopath/bin/db2struct --host 172.20.11.25 -d Message -t UserMessage --package model --struct UserMessageModel -p xx --user xx --json --gorm --target=./model/UserMessageModel.go
package model
import "database/sql"
type UserMessageModel struct {
UserID int `gorm:"column:user_id;primary_key" json:"user_id"` //
ViewTs sql.NullString `gorm:"column:view_ts" json:"view_ts"` //
}
// TableName sets the insert table name for this struct type
func (u *UserMessageModel) TableName() string {
return "UserMessage"
}
func (u *UserMessageModel) DbName() string {
return "Message"
}
(8) 新框架运行流程
main => 解析配置文件 =》 初始化 ucm 、redis 、mysql 、memcache => 注册路由 和 promethus metrics => 监听端口,等待客户端连接

网友评论