• 「项目阅读系列」go-gin-example star 6.5k!(1)


    准备工作

    适宜人群

    初学 go 语法,希望了解 go 项目的构建过程和方式。

    项目信息

    go-gin-example 项目是使用 gin 框架构建一个简易的 blog 服务,包括对 blog 的增删改查操作以及 blog tag 的增删改查等。

    • 代码仓库:https://github.com/eddycjy/go-gin-example
    • 版本:565e1a9395471e829abdb2201e00321c327626cd 第一次提交版本

    项目结构

    项目代码结构如下

    • conf 配置相关
    • middleware 中间件
    • models 数据库相关对象以及操作
    • pkg 项目相关的模块包
    • routers 路由相关
    • main 主函数
      在这里插入图片描述

    代码阅读

    主要模块代码

    首先看一下整体项目中比较重要的模块,包括 主函数、路由模块、授权模块、数据库模块。

    主函数模块

    func main() {
    	router := routers.InitRouter()
    
    	s := &http.Server{
    		Addr:           fmt.Sprintf(":%d", setting.HTTPPort),
    		Handler:        router,
    		ReadTimeout:    setting.ReadTimeout,
    		WriteTimeout:   setting.WriteTimeout,
    		MaxHeaderBytes: 1 << 20,
    	}
    
    	s.ListenAndServe()
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    主函数工作

    • 初始化路由
    • server 配置
    • 启动

    router 路由模块

    func InitRouter() *gin.Engine {
        r := gin.New()
    
        r.Use(gin.Logger())
    
        r.Use(gin.Recovery())
    
        gin.SetMode(setting.RunMode)
    
        r.GET("/auth", api.GetAuth)
    
        apiv1 := r.Group("/api/v1")
        apiv1.Use(jwt.JWT())
        {
            //获取标签列表
            apiv1.GET("/tags", v1.GetTags)
            //新建标签
            apiv1.POST("/tags", v1.AddTag)
            //更新指定标签
            apiv1.PUT("/tags/:id", v1.EditTag)
            //删除指定标签
            apiv1.DELETE("/tags/:id", v1.DeleteTag)
    
            //获取文章列表
            apiv1.GET("/articles", v1.GetArticles)
            //获取指定文章
            apiv1.GET("/articles/:id", v1.GetArticle)
            //新建文章
            apiv1.POST("/articles", v1.AddArticle)
            //更新指定文章
            apiv1.PUT("/articles/:id", v1.EditArticle)
            //删除指定文章
            apiv1.DELETE("/articles/:id", v1.DeleteArticle)
        }
    
        return r
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    主要工作

    • 创建 gin 对象
    • 添加 Logger、Recovery 中间件
      • Logger 日志处理
      • Recovery 异常捕获
    • 设置 /auth 路径的处理器
    • 设置 /api/v1 group,通过 JWT 进行授权验证
    • 设置 /api/v1 下各个请求的处理方式

    auth 授权模块

    type auth struct {
    	Username string `valid:"Required; MaxSize(50)"`
    	Password string `valid:"Required; MaxSize(50)"`
    }
    
    func GetAuth(c *gin.Context) {
    	username := c.Query("username")
    	password := c.Query("password")
    
    	valid := validation.Validation{}
    	a := auth{Username: username, Password: password}
    	ok, _ := valid.Valid(&a)
    
    	data := make(map[string]interface{})
    	code := e.INVALID_PARAMS
    	if ok {
    		isExist := models.CheckAuth(username, password)
    		if isExist {
    			token, err := util.GenerateToken(username, password)
    			if err != nil {
    				code = e.ERROR_AUTH_TOKEN
    			} else {
    				data["token"] = token
    				
    				code = e.SUCCESS
    			}
    
    		} else {
    			code = e.ERROR_AUTH
    		}
    	} else {
    		for _, err := range valid.Errors {
                logging.Info(err.Key, err.Message)
            }
    	}
    
    	c.JSON(http.StatusOK, gin.H{
            "code" : code,
            "msg" : e.GetMsg(code),
            "data" : data,
        })
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42

    工作:

    • 获取用户 username password
    • 验证 username password 是否符合格式
      • 符合,通过 models 进行授权检查,组装 data
      • 不符合,打印错误日志
    • 使用 JSON 格式返回

    数据库

    数据库信息的设置在 models 文件夹下,主要包括 model 文件以及其他具体表文件。

    • model.go 模块通用参数、全局数据库对象、初始化方法&&数据库关闭方法
    • article.go article 对象及其操作方法

    重点看一下 model 文件。

    var db *gorm.DB
    
    type Model struct {
    	ID int `gorm:"primary_key" json:"id"`
    	CreatedOn int `json:"created_on"`
    	ModifiedOn int `json:"modified_on"`
    }
    
    func init() {
    	var (
    		err error
    		dbType, dbName, user, password, host, tablePrefix string
    	)
    
    	sec, err := setting.Cfg.GetSection("database")
    	if err != nil {
    		log.Fatal(2, "Fail to get section 'database': %v", err)
    	}
    
    	dbType = sec.Key("TYPE").String()
    	dbName = sec.Key("NAME").String()
    	user = sec.Key("USER").String()
    	password = sec.Key("PASSWORD").String()
    	host = sec.Key("HOST").String()
    	tablePrefix = sec.Key("TABLE_PREFIX").String()
    
    	db, err = gorm.Open(dbType, fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8&parseTime=True&loc=Local", 
    		user, 
    		password, 
    		host, 
    		dbName))
    
    	if err != nil {
    		log.Println(err)
    	}
    
    	gorm.DefaultTableNameHandler = func (db *gorm.DB, defaultTableName string) string  {
    	    return tablePrefix + defaultTableName;
    	}
    
    	db.SingularTable(true)
    	db.DB().SetMaxIdleConns(10)
    	db.DB().SetMaxOpenConns(100)
    }
    
    func CloseDB() {
    	defer db.Close()
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48

    组成部分

    • 全局变量 db,用于操作数据库
    • Model 结构体,各个数据库表的通用字段
    • init 方法 && closeDB 方法:数据库初始化以及关闭方法

    从中可知,db 主要是通过使用 gorm 框架进行操作,操作中需要设置数据库相关参数以及最大连接数。

    models 下其他具体的数据库表信息在此不做赘述,基本就是数据库表的 struct 定义以及相应操作,下面贴出该项目涉及的三个数据库表及其字段。

    type Article struct {
    	Model
    
    	TagID int `json:"tag_id" gorm:"index"`
    	Tag   Tag `json:"tag"`
    
    	Title string `json:"title"`
    	Desc string `json:"title"`
    	Content string `json:"content"`
    	CreatedBy string `json:"created_by"`
    	ModifiedBy string `json:"modified_by"`
    	State int `json:"state"`
    }
    
    type Tag struct {
    	Model
    
    	Name string `json:"name"`
    	CreatedBy string `json:"created_by"`
    	ModifiedBy string `json:"modified_by"`
    	State int `json:"state"`
    }
    
    type Auth struct {
    	ID int `gorm:"primary_key" json:"id"`
    	Username string `json:"username"`
    	Password string `json:"password"`
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    修改文章请求分析

    主要模块已经了解后,我们查看修改文章 api 的请求过程和具体实现。

    • 请求 URL:/articles/:id
    • 请求方法 :PUT
    • 处理函数: v1.EditArticle

    上述主要代码模块,已经了解到 main 函数启动后,会初始化路由,路由中包含了「修改文章」请求的具体处理函数,这里看看具体函数操作。

    func EditArticle(c *gin.Context) {
    	valid := validation.Validation{}
    
    	id, _ := com.StrTo(c.Param("id")).Int()
    	tagId, _ := com.StrTo(c.Query("tag_id")).Int()
    	title := c.Query("title")
    	desc := c.Query("desc")
    	content := c.Query("content")
    	modifiedBy := c.Query("modified_by")
    
    	var state int = -1
    	if arg := c.Query("state"); arg != "" {
    		state, _ = com.StrTo(arg).Int()
    		valid.Range(state, 0, 1, "state").Message("状态只允许0或1")
    	}
    
    	valid.Min(id, 1, "id").Message("ID必须大于0")
    	valid.MaxSize(title, 100, "title").Message("标题最长为100字符")
        valid.MaxSize(desc, 255, "desc").Message("简述最长为255字符")
        valid.MaxSize(content, 65535, "content").Message("内容最长为65535字符")
    	valid.Required(modifiedBy, "modified_by").Message("修改人不能为空")
        valid.MaxSize(modifiedBy, 100, "modified_by").Message("修改人最长为100字符")
    
      	code := e.INVALID_PARAMS
      	if ! valid.HasErrors() {
      		if models.ExistArticleByID(id) {
      			if models.ExistTagByID(tagId) {
      				data := make(map[string]interface {})
    	  			if tagId > 0 {
    	  				data["tag_id"] = tagId
    	  			}
    	  			if title != "" {
    	  				data["title"] = title
    	  			}
    	  			if desc != "" {
    	  				data["desc"] = desc
    	  			}
    	  			if content != "" {
    	  				data["content"] = content
    	  			}
    
    	  			data["modified_by"] = modifiedBy
    
    	  			models.EditArticle(id, data)
    	  			code = e.SUCCESS
      			} else {
      				code = e.ERROR_NOT_EXIST_TAG
      			}
    		} else {
    			code = e.ERROR_NOT_EXIST_ARTICLE
    		}
    	} else {
    		for _, err := range valid.Errors {
                logging.Info(err.Key, err.Message)
            }
    	}
    
    	c.JSON(http.StatusOK, gin.H{
            "code" : code,
            "msg" : e.GetMsg(code),
            "data" : make(map[string]string),
        })
    }
    
    // models article.go
    func EditArticle(id int, data interface {}) bool {
    	db.Model(&Article{}).Where("id = ?", id).Updates(data)
    
    	return true
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70

    工作:

    • 参数获取
    • 设置校验规则并进行校验
      • 校验成功
        • 文章是否已经存在
          • 是,构建 data 参数,通过 models EditAriticle 修改,具体修改逻辑:找到主键等于 id 的数据,并通过 update 进行更新。
          • 否,设置文章不存在错误码
      • 校验失败
        • 打印错误日志
      • JSON 响应

    其他依赖

    • go-ini 库
      • 该项目通过 ini 文件进行配置管理,go-ini 是 Go 语言中用于操作 ini 文件的第三方库。
    • beego
      • 另一个 go web 框架,项目中主要使用了 beego 的vaild 功能
    • gorm
      • go ORM 框架
    • go-vendor
      • 该项目第一次提交为 18 年,通过 vendor 来管理依赖,现在 go mod 诞生后,这种方式已被放弃

    总结

    该项目第一版提交,大体上完成了 blog 项目所需的基本功能,在目录结构上也相对清晰。

    不足:

    1. 返回信息的 Code、Msg 对象设计比较一般。每次响应需要自己构建响应格式{code;msg;data}。
    2. 配置管理相对粗糙,直接读取配置文件而不是通过 global 统一管理调度。
  • 相关阅读:
    vue 监听页面卷去的高度,获取元素离页面顶部的距离
    【高并发】ScheduledThreadPoolExecutor与Timer的区别和简单示例
    登福布斯小企业推荐邮箱供应商榜单:高效沟通,轻松管理
    实测:游戏情景中,远控软件实力如何?一篇告诉你ToDesk的强大之处
    JAVA如何获取服务器ip
    JavaScript实现鼠标经过图标的浮动
    canvas图像绘制(有放大缩小和拖动功能)
    AWS DynamoDB AWS CLI操作与编程
    OpenHarmony组件复用示例
    STM32驱动AHT10&OLED显示温湿度
  • 原文地址:https://blog.csdn.net/qq_37393071/article/details/134488457