面向初学者的 Golang Restful CRUD
大家好,我是 Sam Zhang。
在上一篇文章中,我们使用 Golang 和 Gin 完成了一个简单的 Hello World 请求。今天,我们来构建一些更复杂的东西——用 Gin 进行 CRUD!
目录
入门
本教程面向具有一定编程经验但对 Go/Gin 相对陌生的开发者。如果您是初学者,可能需要学习一些常见的编程概念。
因此,首先你需要完成我们上一篇文章中提到的 hello world 程序。当然,你也可以从GitHub克隆它来继续。
那我们就開始吧!
CRUD 代表创建、读取、更新和删除。这是数据库中的四种基本操作,今天我们将在我们的 Go 应用中实现它们。
在处理数据库时,您可以编写一些简单的 SQL 命令,然后直接使用数据库驱动程序执行。但这样做有一个问题:SQL 注入攻击。因此,使用 ORM 通常是更好的选择。我们将在本系列文章中使用GORM,因为它很流行且易于上手。
安装 GORM
正如文档中所述,只需使用go get gorm.io/gorm
它来安装即可。
但是,GORM 需要数据库驱动程序才能连接数据库并执行操作。我暂时使用Postgres,您可以使用任何您想要的数据库。
注意:不推荐使用 Sqlite,因为它本身不支持某些复杂的操作。不过目前我们不需要复杂的操作,所以你可以使用它,并且将来可以迁移到其他版本。
因此我们也安装数据库驱动程序:
$ go get -u gorm.io/driver/postgres # or other database provider
...GORM 已准备好使用!
定义数据库模型
要定义模型,你需要声明一个struct
包含表信息的对象。例如:
type <name> struct {
<field> <field_type>
}
是 Go 中定义结构体的最基本形式。我假设你已经对关系数据库存储有一些基础知识,因此我们这里就不做过多讨论了。
为了创建 GORM 模型,我们只需填写所需的信息。如果我们想将博客文章存储在数据库中,那么以下字段可能会有所帮助:
- ID(无符号整数,主键,自动增量,必填)
- 标题(字符串,必填)
- 内容(字符串,必需)
- 创建于(时间,默认为当前时间)
因此,让我们根据上述字段为博客文章创建一个模型:
// models/post.go
package models
import "time"
type Post struct {
ID uint `json:"id" gorm:"primaryKey"`
Title string `json:"title"`
Content string `json:"content"`
CreatedAt time.Time `json:"created_at"`
}
好的,让我解释一下这些。
我们使用结构体定义了一个 GORM 模型,并声明了几个字段。这uint
是unsigned int
其他语言的格式,time.Time
也是 Golang 中的日期时间格式。
但是字段类型后面的反引号对于 Go 新手来说可能有点奇怪。这些字符串被称为标签。他们使用反引号注释来定义键值对。
结构体
tags
是附加到字段的小块元数据,struct
用于向使用该结构体的其他 Go 代码提供指令。1
定义json
key 时,JSON 编码器会将当前字段序列化为 JSON 格式。keygorm
会告知 GORM 一些关于该字段的额外信息。例如,这里我们将其定义ID
为模型的主键。
连接到数据库
现在我们成功创建了数据库模式,让我们将其连接到真实的数据库:
// models/setup.go
package models
import (
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
var DB *gorm.DB
func ConnectDatabase() {
dsn := "host=localhost user=postgres dbname=go_blog port=5432 sslmode=disable timezone=Asia/Shanghai"
database, err := gorm.Open(postgres.Open(dsn), &gorm.Config{}) // change the database provider if necessary
if err != nil {
panic("Failed to connect to database!")
}
database.AutoMigrate(&Post{}) // register Post model
DB = database
}
在函数中ConnectDatabase()
,我们首先定义数据源名称并建立数据库连接。这里我使用了 Postgres 驱动程序,您可以根据自己的需要进行更改。
如果连接数据库时出现任何问题,err
都会指向错误。在这种情况下,我们将调用panic()
来终止整个进程。panic()
这是一个内置函数,其作用类似于raise
Python 和throw
JavaScript 中的函数。
然后我们将Post
模型注册到数据库并“导出”数据库变量。
请注意,这DB
是一个全局变量,可以在包的每个文件中访问models
,从而可以更轻松地进行数据库操作,而无需导入所有内容。
...然后让我们调用我们的连接函数main.go
:
// main.go
package main
import (
"samzhangjy/go-blog/models"
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
models.ConnectDatabase() // new!
// ...
router.Run("localhost:8080")
}
现在我们终于连接到数据库了。是时候编写请求控制器了!
RESTful API
与第一篇文章不同,我们将把所有的请求逻辑放入一个单独的文件夹中controllers
,稍后再导入它们main.go
来定义路由。
创造
让我们从添加一个create
方法开始:
// controllers/post.go
package controllers
import (
"net/http"
"samzhangjy/go-blog/models"
"github.com/gin-gonic/gin"
)
type CreatePostInput struct {
Title string `json:"title" binding:"required"`
Content string `json:"content" binding:"required"`
}
func CreatePost(c *gin.Context) {
var input CreatePostInput
if err := c.ShouldBindJSON(&input); err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
post := models.Post{Title: input.Title, Content: input.Content}
models.DB.Create(&post)
c.JSON(http.StatusOK, gin.H{"data": post})
}
你可能会想,为什么struct
又要用到这个?其实,结构体是 Go 语言中的一个重要概念,你会在本系列文章中多次看到它。在本例中,我们的结构体CreateBlogInput
定义了请求体的 schema CreateBlog
。新的标签binding
是一个基于Validator 的Gin 验证标签。如果你想了解 Gin 绑定,这里有一篇很棒的文章:https://blog.logrocket.com/gin-binding-in-go-a-tutorial-with-example。
接下来我们来关注CreateBlog()
。我们首先使用 验证了请求主体(input
此处为变量)context.ShouldBindJSON()
。如果主体无效,err
则会包含一些错误消息。如果err
包含内容,我们将直接返回 400 HTTP 状态码并中止请求。此if err = statement; err != nil {}
语句是 Go 中常用的错误处理技术。
如果输入有效,我们将首先Post
根据输入的数据创建一个模型。然后调用database.Create()
该方法将该记录放入 Post 表中。
最后,如果一切按预期进行,我们将返回带有新创建的帖子模式的 HTTP 200。
我们将控制器绑定到路由:
// main.go
package main
import (
"samzhangjy/go-blog/controllers"
"samzhangjy/go-blog/models"
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
models.ConnectDatabase()
router.POST("/posts", controllers.CreatePost) // here!
router.Run("localhost:8080")
}
请注意,我们将控制器函数本身传递给router.POST()
,而不带括号。
运行您的应用程序air
并使用 Postman 等工具来试用此端点!
读
然后让我们快速添加一个端点来查看创建的每个帖子:
// controllers/post.go
// ...
func FindPosts(c *gin.Context) {
var posts []models.Post
models.DB.Find(&posts)
c.JSON(http.StatusOK, gin.H{"data": posts})
}
与上一个请求不同,这个请求没有请求体。我们定义了一个数组posts
来存储创建的帖子,类型为models.Post
。DB.Find(&posts)
这意味着查找数据库中存在的每条条目,并将获取的结果存储到posts
。记住要传入指针,而不是实际的变量!
然后快速将其绑定到我们的路由器:
// main.go
package main
import (
"samzhangjy/go-blog/controllers"
"samzhangjy/go-blog/models"
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
models.ConnectDatabase()
router.POST("/posts", controllers.CreatePost)
router.GET("/posts", controllers.FindPosts)
router.Run("localhost:8080")
}
现在您可以看到使用创建的帖子CreateBlog
!
然后让我们创建一个路由,通过 URL 参数仅获取一个指定的帖子:
// controllers/post.go
// ...
func FindPost(c *gin.Context) {
var post models.Post
if err := models.DB.Where("id = ?", c.Param("id")).First(&post).Error; err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"data": post})
}
这里的新方法是DB.Where
和DB.First
。DB.Where()
允许您编写 SQL 查询命令,用替换动态数据?
并将实际数据作为第二个参数传递。DB.First(&post)
与其名称一样,选择给定数据集合的第一个名称并将结果存储在里面post
。
context.Param("<param-name>")
是一个 Gin 方法,用于通过参数名称获取 URL 参数。参数名称定义如下:
// main.go
package main
import (
"samzhangjy/go-blog/controllers"
"samzhangjy/go-blog/models"
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
models.ConnectDatabase()
router.POST("/posts", controllers.CreatePost)
router.GET("/posts", controllers.FindPosts)
router.GET("/posts/:id", controllers.FindPost) // here!
router.Run("localhost:8080")
}
以冒号开头的 slug 的:
定义与 Gin 中的定义相同url parameters
。冒号后面的字符串是参数的名称,我们将使用它来获取参数值。
路线/posts/:id
将匹配以下内容:
/posts/1
/posts/1/
但不会匹配:
/posts/1/abcd
/posts/
更新
然后是时候更新帖子了:
// controllers/post.go
// ...
type UpdatePostInput struct {
Title string `json:"title"`
Content string `json:"content"`
}
func UpdatePost(c *gin.Context) {
var post models.Post
if err := models.DB.Where("id = ?", c.Param("id")).First(&post).Error; err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": "record not found"})
return
}
var input UpdatePostInput
if err := c.ShouldBindJSON(&input); err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
updatedPost := models.Post{Title: input.Title, Content: input.Content}
models.DB.Model(&post).Updates(&updatedPost)
c.JSON(http.StatusOK, gin.H{"data": post})
}
因此,我们再次定义了一个包含请求主体模式的结构体,但这次没有任何验证,因为所有内容都是可选的。我们从上一个方法中复制了验证当前帖子是否存在的代码FindPost()
。
如果帖子存在且请求主体有效,我们将定义一个新模型,包含新生成的帖子数据的内容。在本例中,它的名称是updatedPost
。然后,我们将使用 来获取原始帖子的模型DB.Model(&post)
,并使用 进行更新model.Updates(&updatedPost)
。
model.Updates()
将更新多个字段,并且不会修改更新的架构中未定义的字段(updatedPost
)。model.Update()
一次只会更新一个字段。
并将其绑定到路由器:
// main.go
package main
import (
"samzhangjy/go-blog/controllers"
"samzhangjy/go-blog/models"
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
models.ConnectDatabase()
router.POST("/posts", controllers.CreatePost)
router.GET("/posts", controllers.FindPosts)
router.GET("/posts/:id", controllers.FindPost)
router.PATCH("/posts/:id", controllers.UpdatePost)
router.Run("localhost:8080")
}
删除
最后终于到了DELETE
操作环节:
// controllers/post.go
// ...
func DeletePost(c *gin.Context) {
var post models.Post
if err := models.DB.Where("id = ?", c.Param("id")).First(&post).Error; err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": "record not found"})
return
}
models.DB.Delete(&post)
c.JSON(http.StatusOK, gin.H{"data": "success"})
}
我们需要确保当前给出的帖子 ID 是有效的。然后,我们将调用该函数从数据库中DB.Delete(&post)
删除该条目。post
最后,将其绑定到我们的路由器:
// main.go
package main
import (
"samzhangjy/go-blog/controllers"
"samzhangjy/go-blog/models"
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
models.ConnectDatabase()
router.POST("/posts", controllers.CreatePost)
router.GET("/posts", controllers.FindPosts)
router.GET("/posts/:id", controllers.FindPost)
router.PATCH("/posts/:id", controllers.UpdatePost)
router.DELETE("/posts/:id", controllers.DeletePost)
router.Run("localhost:8080")
}
恭喜!您已成功构建一组简单但可用的 CRUD RESTful API 路由!不妨试用一下,尝试修改一些部分或添加一些新操作!
结论
这是我学习 Go Web 开发的第二部分。实际上,我从撰写这个系列中学到了很多东西,如果文章中有任何错误,请指出!
我已将本文用到的所有源代码上传至GitHub。欢迎大家克隆并试用!
我是Sam Zhang,我们下次再见!
-
摘自[Golang] 结构体标签解释。↩