教程:使用 go 的 gin 和 gorm 框架来构建 RESTful API 微服务
今天我将用 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 :
- POST todos/
- GET todos/
- GET todos/{id}
- PUT todos/{id}
- 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个结构体,命名为 todoModel
、transformedTodo
,第一个结构体代表原始的 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
这个额外的字段时表示什么意思呢?好吧,这里我们来解释一下,这个字段将为我们把 ID
,CreatedAt
,UpdatedAt
和 DeletedAt
这四个字段嵌入到我们定义好的 todoModel
结构体中,一般数据表中都会用到这四个字段。
Gorm 有迁移工具 ,在调用 ‘init’ 函数初始化的时候已经初始化了。当我们运行应用程序时,它将创建一个连接然后进行迁移。
//迁移 schema
db.AutoMigrate(&todoModel{})
使用 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:
获取todos列表:
获取单个todo:
更新单个todo内容:
删除一个todo:
全部的源代码:
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!"})
}
注意:当你使用代码生成时,你必须谨慎操作以下步骤:
- 不要从 todos 中查询所有数据,如:
select * from todos
,使用分页 - 不要相信用户输入,你必须验证输入内容,这里有几种工具来验证输入。 阅读文章 进行验证过程
- 检查每个可能的错误
- 应根据需要来使用日志和用户认证
本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。
高认可度评论:
你的代码看的我,强迫症都来了
大佬,咨询下,是不是api返回的json字段都重新构建一个新的结构体的?因为我最近在写一个web应用,感觉有点疑惑,如果一个接口多,返回的结构都不同,是不是都用新的结构体?想过用map,但是有些数据有点大
你的代码看的我,强迫症都来了
不要再用 gopkg.in 來安裝 gin 了
直接 go get 安装不久好了
不分mvc层吗,直接写到一起了。。。
你的微服务呢?