涉及知识点
- 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 v1
import (
"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 routers
import (
"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-DEFC
F:.
├─.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 models
import (
"github.com/jinzhu/gorm"
)
type Tag struct {
Model
Name 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 constraints
func GetTags(pageNum int, pageSize int, maps interface{}) ([]Tag, error) {
var (
tags []Tag
err 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 constraint
func GetTagTotal(maps interface{})(int,error) {
var count int
if 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 v1
import (
"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 := -1
if 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 name
func ExistTagByName(name string) (bool, error) {
var tag Tag
err := db.Select("id").Where("name = ? AND deleted_on = ? ", name, 0).First(&tag).Error
if err != nil && err != gorm.ErrRecordNotFound {
return false, err
}
if tag.ID > 0 {
return true, nil
}
return false, nil
}
// AddTag Add a Tag
func 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 creating
func 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 updating
func updateTimeStampForUpdateCallback(scope *gorm.Scope) {
if _, ok := scope.Get("gorm:update_column"); !ok {
scope.SetColumn("ModifiedOn", time.Now().Unix())
}
}
// deleteCallback will set `DeletedOn` where deleting
func deleteCallback(scope *gorm.Scope) {
if !scope.HasError() {
var extraOption string
if 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 name
func ExistTagByName(name string) (bool, error) {
var tag Tag
err := db.Select("id").Where("name = ? AND deleted_on = ? ", name, 0).First(&tag).Error
if err != nil && err != gorm.ErrRecordNotFound {
return false, err
}
if tag.ID > 0 {
return true, nil
}
return false, nil
}
// AddTag Add a Tag
func 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 ID
func ExistTagByID(id int) (bool, error) {
var tag Tag
err:=db.Select("id").Where("id=? AND deleted_on=?",id,0).Find(&tag).Error
if err!=nil && err!=gorm.ErrRecordNotFound{
return false,err
}
if tag.ID>0 {
return true, nil
}
return false,nil
}
// EditTag modify a single tag
func 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 tag
func 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