「项目阅读系列」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()
}

主函数工作

  • 初始化路由
  • 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
}

主要工作

  • 创建 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,
    })
}

工作:

  • 获取用户 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()
}

组成部分

  • 全局变量 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"`
}

修改文章请求分析

主要模块已经了解后,我们查看修改文章 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
}

工作:

  • 参数获取
  • 设置校验规则并进行校验
    • 校验成功
      • 文章是否已经存在
        • 是,构建 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 统一管理调度。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值