教程:使用 go 的 gin 和 gorm 框架来构建 RESTful API 微服务

file

今天我将用 golang 编程语言来为我们的 Todo 应用搭建一套简单的 API。我将会使用 Golang 中简便/快捷的 gin-gonic 框架配合用于操作我们数据库,优雅漂亮的 ORM *gorm * 来完成这项工作。要想安装这些包,你应当在工作目录 $GOPATH/src 下运行如下命令:

$ go get github.com/gin-gonic/gin
$ go get -u github.com/jinzhu/gorm
$ go get github.com/go-sql-driver/mysql

在一般的 crud 应用中我们需要如下这样的 API :

  1. POST todos/
  2. GET todos/
  3. GET todos/{id}
  4. PUT todos/{id}
  5. DELETE todos/{id}

我们开始编码吧,去到你的 $GOPATH/src 目录下,创建一个 todo 文件夹。在 todo 文件夹下创建一个名为 main.go 的文件。引入 “gin framework” 到我们的项目中,然后在 main 方法中创建一个类似下面的路由。我比较倾向把 api 前缀写成类似 “api/v1/“ 这样,这就是我们使用 Group 方法的原因。

package main

import (
       "github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
v1 := router.Group("/api/v1/todos")
 {
  v1.POST("/", createTodo)
  v1.GET("/", fetchAllTodo)
  v1.GET("/:id", fetchSingleTodo)
  v1.PUT("/:id", updateTodo)
  v1.DELETE("/:id", deleteTodo)
 }
 router.Run()
}

我们已经创建了五个路由,它们对应的功能处理函数类似 createTodo, fetchAllTodo 等这样。接下来我们会讨论它们。

现在我们需要配置数据库连接。使用数据库前需要引入 gorm 和 mysql dialects 2个软件包。 如下代码:

package main

import (
       "github.com/gin-gonic/gin"
       "github.com/jinzhu/gorm"
       _ "github.com/jinzhu/gorm/dialects/mysql"
)

var db *gorm.DB
func init() {
 //open a db connection
 var err error
 db, err = gorm.Open("mysql", "root:12345@/demo?charset=utf8&parseTime=True&loc=Local")
 if err != nil {
  panic("failed to connect database")
 }
//Migrate the schema
 db.AutoMigrate(&todoModel{})
}

上面代码中 mysql 是数据库驱动,root 是数据库用户名,12345是密码,demo 则是数据库名称。请修改你的数据库连接信息即可。

接下来我们开始编写建立数据库连接相关的代码。首先我们创建2个结构体,命名为 todoModeltransformedTodo ,第一个结构体代表原始的 Todo 数据库字段,第二个结构体用来定义向 api 返回的字段。我们之所以在第二个结构体中重新定义返回的字段主要考虑到数据库中数据的安全性,我们不希望将数据库中的原始字段名(如:updated_at , created_at)直接暴露客户端。

type (
 // 定义原始的数据库字段
 todoModel struct {
  gorm.Model
  Title     string `json:"title"`
  Completed int    `json:"completed"`
 }
// 处理返回的字段
 transformedTodo struct {
  ID        uint   `json:"id"`
  Title     string `json:"title"`
  Completed bool   `json:"completed"`
 }
)

看上述代码有同学可能会有疑问,第一个结构体中多出来一个 gorm.Model 这个额外的字段时表示什么意思呢?好吧,这里我们来解释一下,这个字段将为我们把 IDCreatedAtUpdatedAtDeletedAt 这四个字段嵌入到我们定义好的 todoModel 结构体中,一般数据表中都会用到这四个字段。

Gorm 有迁移工具 ,在调用 ‘init’ 函数初始化的时候已经初始化了。当我们运行应用程序时,它将创建一个连接然后进行迁移。

//迁移 schema
 db.AutoMigrate(&todoModel{})

file

使用 phpmyadmin 可视化工具

你能记得我们前面提到过的五种路由方式吗?现在我们挨个去实现这五种路由方式。

当一个用户用 ‘title 和 completed’ 字段向 ‘api/v1/todos/’ 路径发送一个 PSOT 请求,它将由此路由 ‘v1.POST(「/」,createTodo)‘处理。

接下来让我们继续看一下 createTodo 方法相关的代码书写

// 创建todo
func createTodo(c *gin.Context) {
 completed, _ := strconv.Atoi(c.PostForm("completed"))
 todo := todoModel{Title: c.PostForm("title"), Completed: completed}
 db.Save(&todo)
 c.JSON(http.StatusCreated, gin.H{"status": http.StatusCreated, "message": "Todo item created successfully!", "resourceId": todo.ID})
}

上面的代码中我们使用到了 gin 框架中的 Context 上下文来接收 POST 方式传过来的参数,利用 gorm 连接数据库来保存相关的数据到数据库,之后会给用户返回一个 resourceId

接下来让我们实现剩下的方法

// fetchAllTodo 获取所有Todo
func fetchAllTodo(c *gin.Context) {
 var todos []todoModel
 var _todos []transformedTodo
db.Find(&todos)
if len(todos) <= 0 {
  c.JSON(http.StatusNotFound, gin.H{"status": http.StatusNotFound, "message": "No todo found!"})
  return
 }
//对todos的属性做一些转换以构建更好的响应体
 for _, item := range todos {
  completed := false
  if item.Completed == 1 {
   completed = true
  } else {
   completed = false
  }
  _todos = append(_todos, transformedTodo{ID: item.ID, Title: item.Title, Completed: completed})
 }
 c.JSON(http.StatusOK, gin.H{"status": http.StatusOK, "data": _todos})
}
// fetchSingleTodo 获取单个Todo
func fetchSingleTodo(c *gin.Context) {
 var todo todoModel
 todoID := c.Param("id")
db.First(&todo, todoID)
if todo.ID == 0 {
  c.JSON(http.StatusNotFound, gin.H{"status": http.StatusNotFound, "message": "No todo found!"})
  return
 }
completed := false
 if todo.Completed == 1 {
  completed = true
 } else {
  completed = false
 }
_todo := transformedTodo{ID: todo.ID, Title: todo.Title, Completed: completed}
 c.JSON(http.StatusOK, gin.H{"status": http.StatusOK, "data": _todo})
}
// updateTodo 更新单个todo
func updateTodo(c *gin.Context) {
 var todo todoModel
 todoID := c.Param("id")
db.First(&todo, todoID)
if todo.ID == 0 {
  c.JSON(http.StatusNotFound, gin.H{"status": http.StatusNotFound, "message": "No todo found!"})
  return
 }
db.Model(&todo).Update("title", c.PostForm("title"))
 completed, _ := strconv.Atoi(c.PostForm("completed"))
 db.Model(&todo).Update("completed", completed)
 c.JSON(http.StatusOK, gin.H{"status": http.StatusOK, "message": "Todo updated successfully!"})
}
// deleteTodo 删除一个todo
func deleteTodo(c *gin.Context) {
 var todo todoModel
 todoID := c.Param("id")
db.First(&todo, todoID)
if todo.ID == 0 {
  c.JSON(http.StatusNotFound, gin.H{"status": http.StatusNotFound, "message": "No todo found!"})
  return
 }
db.Delete(&todo)
 c.JSON(http.StatusOK, gin.H{"status": http.StatusOK, "message": "Todo deleted successfully!"})
}

fetchAllTodo 方法中,我们获取了所有的 todos 并且构建了经过转换的响应体,其中包含 id, title, completed 。我们移除了 CreatedAt, UpdatedAt, DeletedAt 字段并将 int 类型转换为bool 类型。

到这里基本业务逻辑的代码已经完成,现在让我们试着构建程序并测试它,我将使用Chrome的Postman扩展来进行测试(你可以使用任何REST客户端进行测试,比如curl)。

要构建应用程序,先打开你的终端,进入项目目录,执行如下命令:

$ go build main.go

上面的命令将构建一个可执行的二进制文件 main ,你可以使用 $ ./main 命令来运行构建号的应用。 哇,我们的todo应用现在运行在了本地的 8080 端口了。 它将在终端显示调试日志,因为 gin 默认以 debug 模式运行在 8080 端口。

要测试API,请先运行 Postman 并依次测试

创建一个todo:

file

获取todos列表:

file

获取单个todo:

file

更新单个todo内容:

file

删除一个todo:

file

全部的源代码:

package main

import (
    "net/http"
    "strconv"

    "github.com/gin-gonic/gin"
    "github.com/jinzhu/gorm"
    _ "github.com/jinzhu/gorm/dialects/mysql"
)

var db *gorm.DB

func init() {
    //创建一个数据库的连接
    var err error
    db, err = gorm.Open("mysql", "root:12345@/demo?charset=utf8&parseTime=True&loc=Local")
    if err != nil {
        panic("failed to connect database")
    }

    //迁移the schema
    db.AutoMigrate(&todoModel{})
}

func main() {

    router := gin.Default()

    v1 := router.Group("/api/v1/todos")
    {
        v1.POST("/", createTodo)
        v1.GET("/", fetchAllTodo)
        v1.GET("/:id", fetchSingleTodo)
        v1.PUT("/:id", updateTodo)
        v1.DELETE("/:id", deleteTodo)
    }
    router.Run()

}

type (
    // todoModel 包括了 todoModel 的字段类型
    todoModel struct {
        gorm.Model
        Title     string `json:"title"`
        Completed int    `json:"completed"`
    }

    // transformedTodo 代表格式化的 todo 结构体
    transformedTodo struct {
        ID        uint   `json:"id"`
        Title     string `json:"title"`
        Completed bool   `json:"completed"`
    }
)

// createTodo 方法添加一条新的 todo 数据
func createTodo(c *gin.Context) {
    completed, _ := strconv.Atoi(c.PostForm("completed"))
    todo := todoModel{Title: c.PostForm("title"), Completed: completed}
    db.Save(&todo)
    c.JSON(http.StatusCreated, gin.H{"status": http.StatusCreated, "message": "Todo item created successfully!", "resourceId": todo.ID})
}

// fetchAllTodo 返回所有的 todo 数据
func fetchAllTodo(c *gin.Context) {
    var todos []todoModel
    var _todos []transformedTodo

    db.Find(&todos)

    if len(todos) <= 0 {
        c.JSON(http.StatusNotFound, gin.H{"status": http.StatusNotFound, "message": "No todo found!"})
        return
    }

    //转化 todos 数据,用来格式化
    for _, item := range todos {
        completed := false
        if item.Completed == 1 {
            completed = true
        } else {
            completed = false
        }
        _todos = append(_todos, transformedTodo{ID: item.ID, Title: item.Title, Completed: completed})
    }
    c.JSON(http.StatusOK, gin.H{"status": http.StatusOK, "data": _todos})
}

// fetchSingleTodo方法返回一条 todo 数据
func fetchSingleTodo(c *gin.Context) {
    var todo todoModel
    todoID := c.Param("id")

    db.First(&todo, todoID)

    if todo.ID == 0 {
        c.JSON(http.StatusNotFound, gin.H{"status": http.StatusNotFound, "message": "No todo found!"})
        return
    }

    completed := false
    if todo.Completed == 1 {
        completed = true
    } else {
        completed = false
    }

    _todo := transformedTodo{ID: todo.ID, Title: todo.Title, Completed: completed}
    c.JSON(http.StatusOK, gin.H{"status": http.StatusOK, "data": _todo})
}

// updateTodo 方法 更新 todo 数据
func updateTodo(c *gin.Context) {
    var todo todoModel
    todoID := c.Param("id")

    db.First(&todo, todoID)

    if todo.ID == 0 {
        c.JSON(http.StatusNotFound, gin.H{"status": http.StatusNotFound, "message": "No todo found!"})
        return
    }

    db.Model(&todo).Update("title", c.PostForm("title"))
    completed, _ := strconv.Atoi(c.PostForm("completed"))
    db.Model(&todo).Update("completed", completed)
    c.JSON(http.StatusOK, gin.H{"status": http.StatusOK, "message": "Todo updated successfully!"})
}

// deleteTodo 方法依据 id 删除一条todo 数据
func deleteTodo(c *gin.Context) {
    var todo todoModel
    todoID := c.Param("id")

    db.First(&todo, todoID)

    if todo.ID == 0 {
        c.JSON(http.StatusNotFound, gin.H{"status": http.StatusNotFound, "message": "No todo found!"})
        return
    }

    db.Delete(&todo)
    c.JSON(http.StatusOK, gin.H{"status": http.StatusOK, "message": "Todo deleted successfully!"})
}

注意:当你使用代码生成时,你必须谨慎操作以下步骤:

  1. 不要从 todos 中查询所有数据,如: select * from todos ,使用分页
  2. 不要相信用户输入,你必须验证输入内容,这里有几种工具来验证输入。 阅读文章 进行验证过程
  3. 检查每个可能的错误
  4. 应根据需要来使用日志和用户认证
本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。

原文地址:https://medium.com/@thedevsaddam/build-r...

译文地址:https://learnku.com/go/t/24598

本帖已被设为精华帖!
本文为协同翻译文章,如您发现瑕疵请点击「改进」按钮提交优化建议
讨论数量: 6

你的代码看的我,强迫症都来了

4年前 评论

大佬,咨询下,是不是api返回的json字段都重新构建一个新的结构体的?因为我最近在写一个web应用,感觉有点疑惑,如果一个接口多,返回的结构都不同,是不是都用新的结构体?想过用map,但是有些数据有点大

4年前 评论
秦穆之 3年前

你的代码看的我,强迫症都来了

4年前 评论
appleboy

不要再用 gopkg.in 來安裝 gin 了

4年前 评论

直接 go get 安装不久好了

3年前 评论
dongguangming

不分mvc层吗,直接写到一起了。。。

3年前 评论

你的微服务呢?

3年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!