基于 Gin 模块化开发 API 框架设计

4,264 阅读4分钟

一、gin项目介绍

gin框架对于写go语言的人来说入门很简单,有点类似python中的flask框架,什么都需要自己去找第三方包,然后根据自己的经验来创建目录结构,对于没有项目经验的人来说,这点真的不如同类型的beego框架,已经有清晰的目录结构,有时候我们可以说gin仅仅是一个包,算不上框架。自由组装度比较灵活,这也提现了我们开发人员的经验重要性。如何更好的搭建gin-api项目是一个难事。

以下是本人根据后端经验采用mvc的思路搭建一套基本的gin-api框架。以供大家参考使用,大家觉得好点个👍

二、需要安装的依赖包

  • gin框架包

    go get -u github.com/gin-gonic/gin
    
  • gorm数据库包

    go get -u gorm.io/gorm
    go get -u gorm.io/driver/mysql
    
  • 数据校验的包

    go get github.com/go-playground/validator
    
  • token认证的包

    go get -u github.com/dgrijalva/jwt-go
    
  • 日志管理包

    go get -u github.com/sirupsen/logrus
    go get -u github.com/lestrrat-go/file-rotatelogs
    go get -u github.com/rifflock/lfshook
    
  • 配置文件的包

    go get -u github.com/spf13/viper
    

三、项目配置文件

  • 1、在config/application.yml文件中创建项目需要的配置参数

    server:
      port: 9000
    # 数据库配置
    datasource:
      driverName: mysql
      host: localhost
      port: "3306"
      database: gin_admin_api
      username: root
      password: 123456
      charset: utf8mb4
      loc: Asia/Shanghai
    
  • 2、在main.go中定义一个初始化配置的文件

    // 初始化配置
    func InitConfig() {
    	workDir, _ := os.Getwd()
    	viper.SetConfigName("application")
    	viper.SetConfigType("yml")
    	viper.AddConfigPath(path.Join(workDir, "config"))
    	// 或者使用全路径
    	//viper.AddConfigPath(path.Join(workDir, "config/application.yml"))
    	err := viper.ReadInConfig()
    	if err != nil {
    		fmt.Print("获取配置文件错误")
    		panic(err)
    	}
    }
    
  • 3、在init函数中调用初始化配置的文件

    func init() {
    	InitConfig()
    }
    
  • 4、测试配置文件是否成功

    func main() {
    	router := gin.Default()
    	router.GET("/", func(c *gin.Context) {
    		c.JSON(http.StatusOK, gin.H{
    			"code": 1,
    		})
    	})
    	port := viper.GetString("server.port")
    	fmt.Println("当前端口", port)
    	if port != "" {
    		router.Run(":" + port)
    	} else {
    		router.Run()
    	}
    }
    
  • 5、或者可以单独到common/config文件中

    package common
    
    import (
    	"fmt"
    	"github.com/spf13/viper"
    	"os"
    	"path"
    )
    
    // 初始化配置
    func InitConfig() {
    	workDir, _ := os.Getwd()
    	viper.SetConfigName("application")
    	viper.SetConfigType("yml")
    	viper.AddConfigPath(path.Join(workDir, "config"))
    	// 或者使用全路径
    	//viper.AddConfigPath(path.Join(workDir, "config/application.yml"))
    	err := viper.ReadInConfig()
    	if err != nil {
    		fmt.Print("获取配置文件错误")
    		panic(err)
    	}
    }
    
    func init() {
    	InitConfig()
    }
    

    借用在main.go中引入的文件,那么初始化就会先执行init函数

    import (
    	...
      // 这里表示编译的时候不需要,但是运行的时候需要,不加这行下面的mian函数中是不能获取到参数的
    	_ "gin_admin_api/common" // gin_admin_api是在go.mod里面配置的module gin_admin_api,一般与项目名称一致
    	...
    )
    
    func main() {
    	...
    	port := viper.GetString("server.port")
    	fmt.Println("当前端口", port)
    	...
    }
    

四、初始化gorm数据库连接工具

  • 1、在common/database下配置数据库连接

    package common
    
    import (
    	"fmt"
    	_ "github.com/go-sql-driver/mysql"
    	"github.com/spf13/viper"
    	"gorm.io/driver/mysql"
    	"gorm.io/gorm"
    	"gorm.io/gorm/logger"
    	"log"
    	"net/url"
    	"os"
    	"time"
    )
    
    var DB *gorm.DB
    
    func init() {
    	fmt.Println("数据库连接")
    	InitDB()
    }
    
    func InitDB() *gorm.DB {
    	// 从配置文件中获取参数
    	host := viper.GetString("datasource.host")
    	port := viper.GetString("datasource.port")
    	database := viper.GetString("datasource.database")
    	username := viper.GetString("datasource.username")
    	password := viper.GetString("datasource.password")
    	charset := viper.GetString("datasource.charset")
    	loc := viper.GetString("datasource.loc")
    	// 字符串拼接
    	sqlStr := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=%s&parseTime=true&loc=%s",
    		username,
    		password,
    		host,
    		port,
    		database,
    		charset,
    		url.QueryEscape(loc),
    	)
    	fmt.Println("数据库连接:", sqlStr)
    	// 配置日志输出
    	newLogger := logger.New(
    		log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer
    		logger.Config{
    			SlowThreshold:             time.Second,   // 缓存日志时间
    			LogLevel:                  logger.Silent, // 日志级别
    			IgnoreRecordNotFoundError: true,          // Ignore ErrRecordNotFound error for logger
    			Colorful:                  false,         // Disable color
    		},
    	)
    	db, err := gorm.Open(mysql.Open(sqlStr), &gorm.Config{
    		Logger: newLogger,
    	})
    	if err != nil {
    		fmt.Println("打开数据库失败", err)
    		panic("打开数据库失败" + err.Error())
    	}
    	DB = db
    	return DB
    }
    
    // TODO 文档地址: https://gorm.io/zh_CN/docs/
    
  • 2、在model/Account.go的数据模型

    package model
    
    import (
    	"gorm.io/gorm"
    )
    
    type Account struct {
    	gorm.Model
    	UserName string `gorm:"type:varchar(50);column(username);not null;unique;comment:账号"`
    	Password string `gorm:"type:varchar(200);not null;comment:账号密码"`
    	Mobile   string `gorm:"varchar(11);not null;unique;comment:手机号码"`
    }
    
  • 3、在main.go中测试创建的数据模型及数据库连接工具

    func init()  {
      // 自动同步数据模型到数据表
    	common.DB.AutoMigrate(&model.Account{})
    }
    
  • 4、查看数据库的数据表这里默认会加上一个s上去,表示复数,如果要重命名表名可以参考下面代码

    // 在数据模型的实体类文件中
    
    // 自定义表名
    func (Account) TableName() string {
    	return "account"
    }
    

五、在gin中使用路由分组实现路由管理

  • 1、创建一个route的文件夹,里面负责收集全部控制器下的路由

    package route
    
    import (
    	"gin_admin_api/controller/account"
    	"gin_admin_api/controller/login"
    	"gin_admin_api/controller/register"
    	"gin_admin_api/middleware"
    	"github.com/gin-gonic/gin"
    )
    
    func CollectRoute(router *gin.Engine) {
    	// 创建账号路由分组,先忽视中间件的存在
    	accountGroup := router.Group("/account", middleware.AuthMiddleWare())
    	account.AccountRouter(accountGroup)
    	// 登录的路由
    	loginGroup := router.Group("/login")
    	login.LoginRouter(loginGroup)
     
    	registerGroup := router.Group("/register")
    	register.RegisterRouter(registerGroup)
    }
    
  • 2、比如登录的路由

    package login
    
    import (
    	"github.com/gin-gonic/gin"
    )
    
    func LoginRouter(router *gin.RouterGroup) {
    	router.POST("/", Login)
    }
    
  • 3、在main.go中使用路由组

    func main() {
    	router := gin.Default()
    	// 注册路由组
    	route.CollectRoute(router)
      ...
    }
    

六、使用数据校验实现用户注册

  • 1、在控制器下创建一个dto的文件,专门用来接收前端传递过来的数据

    package dto
    
    import (
    	"fmt"
    	"gin_admin_api/model"
    	"github.com/go-playground/validator"
    	"unicode/utf8"
    )
    var valildate *validator.Validate
    
    func init() {
    	valildate = validator.New()
    	valildate.RegisterValidation("checkName", CheckNameFunc)
    }
    
    //定义注册的结构体(前端需要发送的数据结构)
    type RegisterDto struct {
    	UserName string `validate:"required,checkName" json:"username"`
    	Password string `validate:"required" json:"password"`
    }
    
    // 自定义校验器校验用户名
    func CheckNameFunc(f validator.FieldLevel) bool {
    	count := utf8.RuneCountInString(f.Field().String())
    	if count >= 2 && count <= 12 {
    		return true
    	} else {
    		return false
    	}
    }
    
    // 定义校验数据的方法
    func ValidatorRegister(account RegisterDto) error {
    	err := valildate.Struct(account)
    	if err != nil {
    		// 输出校验错误 .(validator.ValidationErrors)是断言
    		for _, e := range err.(validator.ValidationErrors)[:1] {
    			fmt.Println("错误字段:", e.Field())
    			fmt.Println("错误的值:", e.Value())
    			fmt.Println("错误的tag:", e.Tag())
    		}
    		return err
    	} else {
    		return nil
    	}
    }
    
  • 2、在控制器中实现将前端传递过来的数据插入到数据库中

    // 用户注册账号
    func Register(c *gin.Context) {
    	// 1.获取前端传递过来的数据
    	var registerDto dto.RegisterDto
    	err := c.Bind(&registerDto)
    	if err != nil {
    		response.Fail(c, "解析前端传递的数据错误")
    		return
    	}
    	// 2.对前端传递过来的数据进行校验
    	err = dto.ValidatorRegister(registerDto)
    	if err != nil {
    		response.Fail(c, "数据校验错误")
    		return
    	}
    	// 3.将数据插入到数据库中
    	newPassword, err := utils.GeneratePassword(registerDto.Password)
    	if err != nil {
    		response.Fail(c, "密码加密错误")
    		return
    	}
      // 4.组装成数据模型的数据结构
    	account := model.Account{
    		UserName: registerDto.UserName,
    		Password: newPassword,
    	}
    	tx := common.DB.Create(&account)
    	fmt.Println(tx.RowsAffected, tx.Error)
    	if tx.RowsAffected > 0 {
    		response.Success(c, nil)
    	} else {
    		response.Fail(c, "插入数据错误")
    	}
    }
    
  • 3、关于密码加密和解密,可以参考utils里面的方法

  • 4、查看数据库是否插入成功

七、关于中间件的使用

  • 1、登录中间件可以参考文章链接地址
  • 2、跨域中间件比较固定,可以直接百度或者参考我百度的数据
  • 3、日志处理可以参考文档链接地址