面向初学者的 Golang Restful CRUD

2025-06-10

面向初学者的 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
Enter fullscreen mode Exit fullscreen mode

...GORM 已准备好使用!

定义数据库模型

与其他 ORM 一样,GORM 使用模型定义表

要定义模型,你需要声明一个struct包含表信息的对象。例如:

type <name> struct {
    <field>  <field_type>
}
Enter fullscreen mode Exit fullscreen mode

是 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"`
}
Enter fullscreen mode Exit fullscreen mode

好的,让我解释一下这些。

我们使用结构体定义了一个 GORM 模型,并声明了几个字段。这uintunsigned int其他语言的格式,time.Time也是 Golang 中的日期时间格式。

但是字段类型后面的反引号对于 Go 新手来说可能有点奇怪。这些字符串被称为标签。他们使用反引号注释来定义键值对。

结构体tags是附加到字段的小块元数据,struct用于向使用该结构体的其他 Go 代码提供指令。1

定义jsonkey 时,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
}
Enter fullscreen mode Exit fullscreen mode

在函数中ConnectDatabase(),我们首先定义数据源名称并建立数据库连接。这里我使用了 Postgres 驱动程序,您可以根据自己的需要进行更改。

如果连接数据库时出现任何问题,err都会指向错误。在这种情况下,我们将调用panic()来终止整个进程。panic()这是一个内置函数,其作用类似于raisePython 和throwJavaScript 中的函数。

然后我们将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")
}
Enter fullscreen mode Exit fullscreen mode

现在我们终于连接到数据库了。是时候编写请求控制器了!

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})
}
Enter fullscreen mode Exit fullscreen mode

你可能会想,为什么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")
}
Enter fullscreen mode Exit fullscreen mode

请注意,我们将控制器函数本身传递给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})
}
Enter fullscreen mode Exit fullscreen mode

与上一个请求不同,这个请求没有请求体。我们定义了一个数组posts来存储创建的帖子,类型为models.PostDB.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")
}
Enter fullscreen mode Exit fullscreen mode

现在您可以看到使用创建的帖子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})
}
Enter fullscreen mode Exit fullscreen mode

这里的新方法是DB.WhereDB.FirstDB.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")
}
Enter fullscreen mode Exit fullscreen mode

以冒号开头的 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})
}
Enter fullscreen mode Exit fullscreen mode

因此,我们再次定义了一个包含请求主体模式的结构体,但这次没有任何验证,因为所有内容都是可选的。我们从上一个方法中复制了验证当前帖子是否存在的代码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")
}
Enter fullscreen mode Exit fullscreen mode

删除

最后终于到了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"})
}
Enter fullscreen mode Exit fullscreen mode

我们需要确保当前给出的帖子 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")
}
Enter fullscreen mode Exit fullscreen mode

恭喜!您已成功构建一组简单但可用的 CRUD RESTful API 路由!不妨试用一下,尝试修改一些部分或添加一些新操作!

结论

这是我学习 Go Web 开发的第二部分。实际上,我从撰写这个系列中学到了很多东西,如果文章中有任何错误,请指出!

我已将本文用到的所有源代码上传至GitHub。欢迎大家克隆并试用!

我是Sam Zhang,我们下次再见!


  1. 摘自[Golang] 结构体标签解释。↩ 

鏂囩珷鏉ユ簮锛�https://dev.to/samzhangjy/restful-crud-with-golang-for-beginners-23ia
PREV
使用策略模式(C# 示例)先决条件示例问题输入策略模式。踩刹车。
NEXT
VueBlogger:Vue 的博客网站生成器