涉及知识点
- Gin:Golang 的一个微框架,性能极佳。
- beego-validation:本节采用的 beego 的表单验证库,中文文档。
- gorm,对开发人员友好的 ORM 框架,英文文档
- com,一个小而美的工具包。
本文目标
- 完成博客的标签类接口定义和编写
定义接口
本节正是编写标签的逻辑,我们想一想,一般接口为增删改查是基础的,那么我们定义一下接口吧!
- 获取标签列表:GET(“/tags”)
- 新建标签:POST(“/tags”)
- 更新指定标签:PUT(“/tags/:id”)
- 删除指定标签:DELETE(“/tags/:id”)
编写路由空壳
开始编写路由文件逻辑,在routers下新建api目录,我们当前是第一个 API 大版本,因此在api下新建v1目录,再新建tag.go文件,写入内容:
package v1import ("github.com/gin-gonic/gin")//获取多个文章标签func GetTags(c *gin.Context) {}// @Summary 新增文章标签// @Produce json// @Param name query string true "Name"// @Param state query int false "State"// @Param created_by query int false "CreatedBy"// @Success 200 {string} json "{"code":200,"data":{},"msg":"ok"}"// @Router /api/v1/tags [post]func AddTag(c *gin.Context) {}// @Summary 修改文章标签// @Produce json// @Param id path int true "ID"// @Param name query string true "ID"// @Param state query int false "State"// @Param modified_by query string true "ModifiedBy"// @Success 200 {string} json "{"code":200,"data":{},"msg":"ok"}"// @Router /api/v1/tags/{id} [put]func EditTag(c *gin.Context) {}//删除文章标签func DeleteTag(c *gin.Context) {}
注册路由
我们打开routers下的router.go文件,修改文件内容为:
package routersimport ("github.com/gin-gonic/gin""github.com/noobwu/go-gin-demo/routers/api/v1"_ "github.com/noobwu/go-gin-demo/docs""github.com/swaggo/gin-swagger""github.com/swaggo/gin-swagger/swaggerFiles")func InitRouter() *gin.Engine {r := gin.New()r.Use(gin.Logger())r.Use(gin.Recovery())r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))apiv1 := r.Group("/api/v1"){//获取标签列表apiv1.GET("/tags", v1.GetTags)//新建标签apiv1.POST("/tags", v1.AddTag)//更新指定标签apiv1.PUT("/tags/:id", v1.EditTag)//删除指定标签apiv1.DELETE("/tags/:id", v1.DeleteTag)}return r}
当前目录结构:
PS F:\Projects\NoobWu\go-samples\go-gin-demo> tree文件夹 PATH 列表卷序列号为 0001-DEFCF:.├─.idea├─conf├─docs├─middleware├─models├─pkg│ └─setting├─routers│ └─api│ └─v1└─runtime
检验路由是否注册成功
回到命令行,执行 go run main.go,检查路由规则是否注册成功。
PS F:\Projects\NoobWu\go-samples\go-gin-demo> go run main.go

运行成功,那么我们愉快的开始编写我们的接口吧!

下载依赖包
首先我们要拉取 validation 的依赖包,在后面的接口里会使用到表单验证
$ go get -u github.com/astaxie/beego/validation
PS D:\Projects\Github\NoobWu\go-samples\go-gin-demo> go get -u github.com/astaxie/beego/validation
编写标签列表的 models 逻辑
**D:\Projects\Github\NoobWu\go-samples\go-gin-demo\models\tag.go**
创建 models 目录下的 tag.go,写入文件内容:
package modelsimport ("github.com/jinzhu/gorm")type Tag struct {ModelName string `json:"name"`CreatedBy string `json:"created_by"`ModifiedBy string `json:"modified_by"`State int `json:"state"`}// GetTags gets a list of tags based on paging and constraintsfunc GetTags(pageNum int, pageSize int, maps interface{}) ([]Tag, error) {var (tags []Tagerr error)if pageSize > 0 && pageNum > 0 {err = db.Where(maps).Find(&tags).Offset(pageNum).Limit(pageSize).Error} else {err = db.Where(maps).Find(&tags).Error}if err != nil && err != gorm.ErrRecordNotFound {return nil, err}return tags, nil}// GetTagTotal counts the total number of tags based on the constraintfunc GetTagTotal(maps interface{})(int,error) {var count intif err:=db.Model(&Tag{}).Where(maps).Count(&count).Error;err != nil {return 0,err}return count,nil}
- 我们创建了一个
Tag struct{},用于Gorm的使用。并给予了附属属性json,这样子在c.JSON的时候就会自动转换格式,非常的便利 - 可能会有的初学者看到
return,而后面没有跟着变量,会不理解;其实你可以看到在函数末端,我们已经显示声明了返回值,这个变量在函数体内也可以直接使用,因为他在一开始就被声明了 - 有人会疑惑
db是哪里来的;因为在同个models包下,因此db *gorm.DB是可以直接使用的
编写标签列表的路由逻辑
打开 routers 目录下 v1 版本的 tag.go,第一我们先编写获取标签列表的接口
修改文件内容:D:\Projects\Github\NoobWu\go-samples\go-gin-demo\routers\api\v1\tag.go
package v1import ("github.com/gin-gonic/gin""github.com/noobwu/go-gin-demo/pkg/util""github.com/unknwon/com""net/http""github.com/noobwu/go-gin-demo/pkg/app""github.com/noobwu/go-gin-demo/pkg/e""github.com/noobwu/go-gin-demo/pkg/setting""github.com/noobwu/go-gin-demo/service/tag_service")// @Summary Get multiple article tags// @Produce json// @Param name query string false "Name"// @Param state query int false "State"// @Success 200 {object} app.Response// @Failure 500 {object} app.Response// @Router /api/v1/tags [get]func GetTags(c *gin.Context) {appG := app.Gin{C: c}name := c.Query("name")state := -1if arg := c.Query("state"); arg != "" {state = com.StrTo(arg).MustInt()}tagService := tag_service.Tag{Name: name,State: state,PageNum: util.GetPage(c),PageSize: setting.AppSetting.PageSize,}tags, err := tagService.GetAll()if err != nil {appG.Response(http.StatusInternalServerError, e.ERROR_GET_TAGS_FAIL, nil)return}count, err := tagService.Count()if err != nil {appG.Response(http.StatusInternalServerError, e.ERROR_COUNT_TAG_FAIL, nil)return}appG.Response(http.StatusOK, e.SUCCESS, map[string]interface{}{"lists": tags,"total": count,})}// @Summary 新增文章标签// @Produce json// @Param name query string true "Name"// @Param state query int false "State"// @Param created_by query int false "CreatedBy"// @Success 200 {string} json "{"code":200,"data":{},"msg":"ok"}"// @Router /api/v1/tags [post]func AddTag(c *gin.Context) {}// @Summary 修改文章标签// @Produce json// @Param id path int true "ID"// @Param name query string true "ID"// @Param state query int false "State"// @Param modified_by query string true "ModifiedBy"// @Success 200 {string} json "{"code":200,"data":{},"msg":"ok"}"// @Router /api/v1/tags/{id} [put]func EditTag(c *gin.Context) {}//删除文章标签func DeleteTag(c *gin.Context) {}
c.Query可用于获取?name=test&state=1这类 URL 参数,而c.DefaultQuery则支持设置一个默认值code变量使用了e模块的错误编码,这正是先前规划好的错误码,方便排错和识别记录util.GetPage保证了各接口的page处理是一致的c *gin.Context是Gin很重要的组成部分,可以理解为上下文,它允许我们在中间件之间传递变量、管理流、验证请求的 JSON 和呈现JSON响应
在本机执行 curl 127.0.0.1:7000/api/v1/tags ,正确的返回值为{"code":200,"data":{"lists":[],"total":0},"msg":"ok"},若存在问题请结合 gin 结果进行拍错。
curl 127.0.0.1:7000/api/v1/tags

Invoke-WebRequest -Uri "127.0.0.1:7000/api/v1/tags" -UseBasicParsing

在获取标签列表接口中,我们可以根据 name、state、page 来筛选查询条件,分页的步长可通过 app.ini 进行配置,以lists、total 的组合返回达到分页效果。
编写新增标签的 models 逻辑
接下来我们编写新增标签的接口
打开 models 目录下的 **tag.go**,修改文件(增加 2 个方法):D:\Projects\Github\NoobWu\go-samples\go-gin-demo\models\tags.go
// ExistTagByName checks if there is a tag with the same namefunc ExistTagByName(name string) (bool, error) {var tag Tagerr := db.Select("id").Where("name = ? AND deleted_on = ? ", name, 0).First(&tag).Errorif err != nil && err != gorm.ErrRecordNotFound {return false, err}if tag.ID > 0 {return true, nil}return false, nil}// AddTag Add a Tagfunc AddTag(name string, state int, createBy string) error {tag := Tag{Name: name,State: state,CreatedBy: createBy,}if err := db.Create(&tag).Error; err != nil {return err}return nil}
编写新增标签的路由逻辑
打开 **routers** 目录下的**tag.go**,修改文件(变动 AddTag 方法):D:\Projects\Github\NoobWu\go-samples\go-gin-demo\routers\api\v1\tag.go
type AddTagForm struct {Name string `form:"name" valid:"Required;MaxSize(100)"`CreatedBy string `form:"created_by" valid:"Required;MaxSize(100)"`State int `form:"state" valid:"Range(0,1)"`}// @Summary 新增文章标签// @Produce json// @Param name query string true "Name"// @Param state query int false "State"// @Param created_by query int false "CreatedBy"// @Success 200 {string} json "{"code":200,"data":{},"msg":"ok"}"// @Router /api/v1/tags [post]func AddTag(c *gin.Context) {var (appG = app.Gin{C: c}form AddTagForm)httpCode, errCode := app.BindAndValid(c, &form)if errCode != e.SUCCESS {appG.Response(httpCode, errCode, nil)return}tagService:=tag_service.Tag{Name: form.Name,CreatedBy: form.CreatedBy,State: form.State,}exists,err:=tagService.ExistByName()if err!=nil{appG.Response(http.StatusInternalServerError,e.ERROR_EXIST_TAG_FAIL,nil)return}if exists{appG.Response(http.StatusOK,e.ERROR_EXIST_TAG,nil)return}err =tagService.Add()if err!=nil{appG.Response(http.StatusInternalServerError,e.ERROR_ADD_TAG_FAIL,nil)return}appG.Response(http.StatusOK,e.SUCCESS,nil)}
用Postman用 POST 访问 http://127.0.0.1:7000/api/v1/tags?name=tag_test&state=1&created_by=admin,查看 code 是否返回**200**及**blog_tag** 表中是否有值,有值则正确。
curl --location --request POST 'http://127.0.0.1:7000/api/v1/tags' \--header 'Content-Type: application/x-www-form-urlencoded' \--data-urlencode 'name=tag_test' \--data-urlencode 'created_by=admin' \--data-urlencode 'state=s'

$headers = New-Object "System.Collections.Generic.Dictionary[[String],[String]]"$headers.Add("Content-Type", "application/x-www-form-urlencoded")$body = "name=tag_test&created_by=admin&state=1"$response = Invoke-RestMethod 'http://127.0.0.1:7000/api/v1/tags' -Method 'POST' -Headers $headers -Body $body$response | ConvertTo-Json
编写 models callbacks
但是这个时候大家会发现,我明明新增了标签,但 created_on 居然没有值,那做修改标签的时候modified_on 会不会也存在这个问题?
为了解决这个问题,我们需要打开 models 目录下的 tag.go 文件,修改文件内容(修改包引用和增加 2 个方法):D:\Projects\Github\NoobWu\go-samples\go-gin-demo\models\models.go
// updateTimeStampForCreateCallback will set `CreatedOn`, `ModifiedOn` when creatingfunc updateTimeStampForCreateCallback(scope *gorm.Scope) {if !scope.HasError() {nowTime := time.Now().Unix()if createTimeField, ok := scope.FieldByName("CreatedOn"); ok {if createTimeField.IsBlank {createTimeField.Set(nowTime)}}if modifyTimeField, ok := scope.FieldByName("ModifiedOn"); ok {if modifyTimeField.IsBlank {modifyTimeField.Set(nowTime)}}}}// updateTimeStampForUpdateCallback will set `ModifiedOn` when updatingfunc updateTimeStampForUpdateCallback(scope *gorm.Scope) {if _, ok := scope.Get("gorm:update_column"); !ok {scope.SetColumn("ModifiedOn", time.Now().Unix())}}// deleteCallback will set `DeletedOn` where deletingfunc deleteCallback(scope *gorm.Scope) {if !scope.HasError() {var extraOption stringif str, ok := scope.Get("gorm:delete_option"); ok {extraOption = fmt.Sprint(str)}deletedOnField, hasDeletedOnField := scope.FieldByName("DeletedOn")if !scope.Search.Unscoped && hasDeletedOnField {scope.Raw(fmt.Sprintf("UPDATE %v SET %v=%v%v%v",scope.QuotedTableName(),scope.Quote(deletedOnField.DBName),scope.AddToVars(time.Now().Unix()),addExtraSpaceIfExist(scope.CombinedConditionSql()),addExtraSpaceIfExist(extraOption),)).Exec()} else {scope.Raw(fmt.Sprintf("DELETE FROM %v%v%v",scope.QuotedTableName(),addExtraSpaceIfExist(scope.CombinedConditionSql()),addExtraSpaceIfExist(extraOption),)).Exec()}}}
重启服务,再在用 Postman 用 POST 访问 http://127.0.0.1:8000/api/v1/tags?name=2&state=1&created_by=test,发现 created_on 已经有值了!
在这几段代码中,涉及到知识点:
这属于 gorm 的 Callbacks,可以将回调方法定义为模型结构的指针,在创建、更新、查询、删除时将被调用,如果任何回调返回错误,gorm 将停止未来操作并回滚所有更改。
gorm所支持的回调方法:
- 创建:BeforeSave、BeforeCreate、AfterCreate、AfterSave
- 更新:BeforeSave、BeforeUpdate、AfterUpdate、AfterSave
- 删除:BeforeDelete、AfterDelete
- 查询:AfterFind
编写其余接口的路由逻辑
接下来,我们一口气把剩余的两个接口(EditTag、DeleteTag)完成吧
打开 routers 目录下 **v1** 版本的 tag.go 文件,修改内容:D:\Projects\Github\NoobWu\go-samples\go-gin-demo\routers\api\v1\tag.go
//EditTag// @Summary 修改文章标签// @Produce json// @Param id path int true "ID"// @Param name query string true "ID"// @Param state query int false "State"// @Param modified_by query string true "ModifiedBy"// @Success 200 {string} json "{"code":200,"data":{},"msg":"ok"}"// @Router /api/v1/tags/{id} [put]func EditTag(c *gin.Context) {var (appG = app.Gin{C: c}form = EditTagForm{ID: com.StrTo(c.Param("id")).MustInt()})//校验请求参数httpCode, errCode := app.BindAndValid(c, &form)if errCode != e.SUCCESS {appG.Response(httpCode, errCode, nil)return}tagService:=tag_service.Tag{ID: form.ID,Name: form.Name,State: form.State,ModifiedBy: form.ModifiedBy,}exists,err:=tagService.ExistByID()if err!=nil{appG.Response(http.StatusInternalServerError,e.ERROR_EXIST_TAG_FAIL,nil)return}if !exists{appG.Response(http.StatusOK,e.ERROR_NOT_EXIST_TAG,nil)return}err =tagService.Edit()if err!=nil{appG.Response(http.StatusInternalServerError,e.ERROR_EDIT_TAG_FAIL,nil)return}appG.Response(http.StatusOK,e.SUCCESS,nil)}// DeleteTag// @Summary 删除文章标签// @Produce json// @Param id path int true "ID"// @Success 200 {object} app.Response// @Failure 500 {object} app.Response// @Router /api/v1/tags/{id} [delete]func DeleteTag(c *gin.Context) {appG := app.Gin{C: c}valid := validation.Validation{}id := com.StrTo(c.Param("id")).MustInt()valid.Min(id, 1, "id").Message("ID必须大于0")if valid.HasErrors() {app.MarkErrors(valid.Errors)appG.Response(http.StatusBadRequest, e.INVALID_PARAMS, nil)}tagService := tag_service.Tag{ID: id}exists, err := tagService.ExistByID()if err != nil {appG.Response(http.StatusInternalServerError, e.ERROR_EXIST_TAG_FAIL, nil)return}if !exists {appG.Response(http.StatusOK, e.ERROR_NOT_EXIST_TAG, nil)return}if err := tagService.Delete(); err != nil {appG.Response(http.StatusInternalServerError, e.ERROR_DELETE_TAG_FAIL, nil)return}appG.Response(http.StatusOK, e.SUCCESS, nil)}
编写其余接口的 models 逻辑
打开 **models** 下的 **tag.go**,修改文件内容:D:\Projects\Github\NoobWu\go-samples\go-gin-demo\models\tags.go
// ExistTagByName checks if there is a tag with the same namefunc ExistTagByName(name string) (bool, error) {var tag Tagerr := db.Select("id").Where("name = ? AND deleted_on = ? ", name, 0).First(&tag).Errorif err != nil && err != gorm.ErrRecordNotFound {return false, err}if tag.ID > 0 {return true, nil}return false, nil}// AddTag Add a Tagfunc AddTag(name string, state int, createBy string) error {tag := Tag{Name: name,State: state,CreatedBy: createBy,}if err := db.Create(&tag).Error; err != nil {return err}return nil}// ExistTagByID determines whether a Tag exists based on the IDfunc ExistTagByID(id int) (bool, error) {var tag Tagerr:=db.Select("id").Where("id=? AND deleted_on=?",id,0).Find(&tag).Errorif err!=nil && err!=gorm.ErrRecordNotFound{return false,err}if tag.ID>0 {return true, nil}return false,nil}// EditTag modify a single tagfunc EditTag(id int, data interface{}) error {if err := db.Model(&Tag{}).Where("id = ? AND deleted_on = ? ", id, 0).Updates(data).Error; err != nil {return err}return nil}// DeleteTag delete a tagfunc DeleteTag(id int) error {if err:=db.Where("id=?",id).Delete(&Tag{}).Error;err!=nil{return err}return nil}
验证功能
重启服务,用 Postman
- PUT 访问
http://127.0.0.1:7000/api/v1/tags/1?name=edit1&state=0&modified_by=edit1,查看 code 是否返回 200 - DELETE 访问
http://127.0.0.1:7000/api/v1/tags/1,查看 code 是否返回 200
至此,Tag 的 API’s 完成,下一节我们将开始 Article 的 API’s 编写!
go-gin.postman_collection.json
