亲手写一个gin server项目-3路由及目录组织

855 阅读4分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第6天,点击查看活动详情

本系列文章旨在从头到尾写一个admin的gin server项目。

整个项目包括:项目结构,项目启动,配置文件,中间件,日志,格式化响应,表单校验,检索,jwt会话,rbac鉴权,单元测试,文档生成,基于es的单体日志采集,基于prometheus的指标监控......。

所涉及到的技术栈包括:gin、zap、mysql、gorm、redis、es、prometheus 等。

今天介绍路由及目录组织。

项目目录

前面为了方便讲解和代码复现,所有的代码都在一个文件里。

写的handler多了,接口多了,都在一个文件里肯定不合适的。

下面是一个完整的项目目录结构:

1669649351595.png

目录功能解释:

  • cmd 程序入口,目前有httpserver和管理员创建两个功能。
  • config 配置文件存放的地方,config.yaml是全局配置文件,logmapping.json是日志文件的mapping配置。
  • internal 是整个项目内容所在目录,go中internal目录用来对代码进行访问控制。
    • app 是整个项目接口及数据库及路由编写的地方。
    • apptest 是对app中的接口进行测试的地方。
    • config 是config struct 定义的地方。
    • middler 是中间件存放的地方。
    • pkg 是工具代码存放的地方。
    • router 基础路及其路由中间件代码。
  • log 日志输出位置。
  • script 一些自动化脚本存放地方。
  • swagdoc swagger json文件存放的地方。
  • main.go 整个项目入口文件。
  • Taskfile.yml 自动化测试,文档生成的任务编排配置文件。

整个目录结构中规中矩,比较容易理解。

整个项目有比较完整的测试,文档生成方案。为了自动化完成这些操作,使用了task一个go写的自动化任务软件。那个Taskfile.yml正是这个软件的配置文件。

重构main.go入口文件

入口文件是个启动文件,最主要功能是获取参数,包括命令行参数,配置文件参数,只有获取了这些参数。整个程序才能继续进行,代码如下。

var configFile = flag.String("f", "./config/config.yaml", "the config file")

func main() {
   flag.Usage = Usage
   flag.Parse()

   var c config.Config
   yamconfig.MustLoad(*configFile, &c)
   if len(os.Args) < 2 {
      flag.Usage()
      os.Exit(1)
   }

   cmder := os.Args[1]
   switch cmder {
   case "run":
      cmd.Server(c)
   case "super":
      super := cmd.NewSuperman(c)
      if err := super.CreateSuperman(); err != nil {
         log.Println(err)
      }
    default:
        flag.Usage()
        os.Exit(1)
   }
}

func Usage() {
   fmt.Println("go server v1.0")
   fmt.Println("main [cmd] [tag]")
   fmt.Println("cmd:")
   fmt.Println("   run: server run")
    fmt.Println("   super: create or update super account")
   fmt.Println("tag:")
   flag.PrintDefaults()
}

运行命令 go run main.go run 即可把服务启动,运行go run main.go super即可创建管理员或者重置超级管理员密码。

从代码中可以看出,首先初始化了配置文件对象var c config.Config,这个配置文件对象带入到cmd.Server中。

服务启动代码 cmd/server.go

cmd/server.go 是整个httpserver的启动逻辑代码。代码如下:

package cmd

import (
   "myadmin/internal/app"
   "myadmin/internal/config"
   "myadmin/internal/middler"
   "myadmin/internal/serctx"
   "github.com/gin-gonic/gin"
   "gs/api/apiserver"
   "gs/pkg/metric"
)

func Server(c config.Config) {
   //资源初始化
   sc, err := serctx.NewServerContext(c)
   if err != nil {
      panic(err)
   }
   //服务 中间件
   engine := gin.New()

   apiserver.RegMiddler(engine,
      apiserver.WithMiddle(middler.RegMiddler(sc)...),
      apiserver.WithStatic("/view", c.Api.ViewDir),
   )

   //启动promagent
   metric.StartAgent(engine, "/metrics", sc.Config.Prom.UserName, sc.Config.Prom.Password)

   //注册路由
   app.RegisterRoute(engine, sc)

   //注册数据库
   app.Regdb(sc)

   //启动
   apiserver.Run(engine, sc.Log.Logger, c.Api)
}

重点看这段代码

 //资源初始化
   sc, err := serctx.NewServerContext(c)
   if err != nil {
      panic(err)
   }

这部分在第一篇文章中项目启动也介绍过。主要是启动或者初始化相关资源,此项目就是启动数据库连接,启动redis,配置数据库对象。

serctx/serctx.go

代码如下:

package serctx

import (
   "myadmin/internal/config"
   "gs/pkg/logx"
   "gs/pkg/mysqlx"
   "gs/pkg/redisx"
   "github.com/go-redis/redis/v8"
   errs "github.com/pkg/errors"
   "gorm.io/gorm"
)

//所有资源放在此处
type ServerContext struct {
   Config config.Config
   Log    *logx.Logx
   Db     *gorm.DB
   Redis  *redis.Client
}

func NewServerContext(c config.Config) (*ServerContext, error) {
   //初始化日志
   sc := &ServerContext{}
   sc.Config = c
   if lg, err := logx.NewLogx(c.Log); err != nil {
      return nil, err
   } else {
      sc.Log = lg
   }

   //初始化数据库
   db := mysqlx.NewDb(c.Mysql)
   if d, err := db.GetDb(); err != nil {
      return nil, errs.WithMessage(err, "err init db")
   } else {
      d.Debug()
      sc.Db = d
      sc.Log.Info("数据库初始化完成")

   }

   //初始化redis
   if redisCli, err := redisx.NewRedis(c.Redis).GetDb(); err != nil {
      return nil, errs.WithMessage(err, "err init redis")
   } else {
      sc.Redis = redisCli
      sc.Log.Info("redis初始化完成")

   }
   
   return sc, nil
}

这个serverContext很重要,后期需要添加任何外部模块,都可以在此处进行初始化,并添加到ServerContext对象中。这个对象会通过路由注入到所有的handler中。使用起来,修改起来都很方便。

路由

整套路由设计把返回数据进行统计格式化处理。各个模块路由由模块处定义。

在上边server.go代码中,有一行如有注册函数。

app.RegisterRoute(engine, sc)

这个app就是那个app目,所有api编写的地方,看一下app下边的目录结构,目前只有一个admin模块,因为该项目只是写一个admin管理平台:

60ce7f4386e4e144780538f6f83c078.png

regrouter.go 是各个app注册的总路由注册地方。 代码如下:

package app

import (
   "github.com/gin-gonic/gin"
   "myadmin/internal/app/admin"
   "myadmin/internal/router"
   "myadmin/internal/serctx"
)

var routes = []func(r *router.Router, sc *serctx.ServerContext){
   //所有路由按照app注册在此
   admin.Route,
}

func RegisterRoute(engine *gin.Engine, sc *serctx.ServerContext) {
   r := router.NewRouter(engine, sc)
   for _, v := range routes {
      v(r, sc)
   }
}

再看一下admin的router.go路由:

d39a4651b2176c41e7ae0037896c159.png

所有admin的路由及handler整齐写在一块。

这就是整个路由链路。

还有两个很重的地方没有解释。

基础路由就是那个router目录下的东西。

1669652116159.png

这部分主要两个作用,一个是定义基础路由的中间件,一个是统计管理handler的异常返回。

基础如有中间件管理在router/router.go里,代码如下:

/**
路由基础定义
*/

type Router struct {
   Root *gin.RouterGroup
   Jwt  *gin.RouterGroup //jwt登陆
}

func NewRouter(g *gin.Engine, sc *serctx.ServerContext) *Router {
   return &Router{
      Root: g.Group("/"),
      Jwt:  g.Group("/api", middler.TokenPase(sc), middler.LoginCode(sc)),
   }
}

从代码中可以看出,凡是Jwt开头的都有 "/api"前缀,并且带上了token解析和,唯一登录的中间件(一台机子登陆后,另一台同个账号登陆,则第一台机子账号下线。),而root路由则没有任何限制。

异常返回统一处理在 router/do.go 中代码如下:

1669652465236.png

从代码中可以看出,handler如果返回错误,如果类型不是hd.ErrResponse则会将异常字符串化直接返回,其实非多语言中,handler直接 return errors.New("xxxxx")就能把想要的异常信息返还给前端。

整个路由介绍完了,下面介绍一下handler的编写。

1669652839352.png

admin的handler很多,除了账号的增删查个,还有个人的查改登陆登出,token管理等。

拿一个账号检索(get.go)的代码看看:

1669652967047.png

把用到的资源都写在struct中 ,handler中所有用到的东西都直接拿来用。

结尾

这篇文章主要讲解了目录结构和路由。 下次讲讲中间件。