教程:使用 Go 和 Gin 开发 RESTful API

本教程介绍了使用 Go 和 Gin Web 框架 (Gin)编写 RESTful Web 服务 API 的基础知识.

如果您对 Go 及其工具有基本的了解,您将从本教程中获得最大收益。如果这是您第一次接触 Go,请参阅 教程:Go 入门以获取 快速介绍。

Gin 简化了许多与构建 Web 应用程序(包括 Web 服务)相关的编码任务。在本教程中,您将使用 Gin 路由请求、检索请求详细信息以及组织 JSON 以进行响应。

在本教程中,您将构建一个具有两个端点的 RESTful API 服务器。您的示例项目将是有关复古爵士乐唱片的数据存储库.

本教程包括以下部分:

  1. 设计 API 端点.
  2. 为您的代码创建一个文件夹.
  3. 创建数据.
  4. 编写一个处理程序来返回所有项目(item).
  5. 编写一个处理程序来添加一个新项目.
  6. 编写一个处理程序来返回一个特定的项目.

注意: 有关其他教程,请参阅 教程.

要尝试将此作为您在 Google Cloud Shell 中完成的交互式教程,请点击下面的按钮.

Open in Cloud Shell

先决条件

设计 API 端点

您将构建一个 API,该 API 提供对销售黑胶唱片老式唱片的商店的访问。因此,您需要提供端点,客户端可以通过这些端点为用户获取和添加专辑.

在开发 API 时,您通常从设计端点开始。如果端点易于理解,您的 API 用户将获得更多成功.

以下是您将在本教程中创建的端点.

/albums

/albums/:id

接下来,您将为您的代码创建一个文件夹.

为您的代码创建一个文件夹

首先,为您将编写的代码创建一个项目(project).

  1. 打开命令提示符并切换到您的home目录.

    在 Linux 或 Mac 上:

    $ cd
    

    在 Windows 上:

    C:\> cd %HOMEPATH%
    
  2. 使用命令提示符为您的代码创建一个名为 web-service-gin 的目录.

    $ mkdir web-service-gin
    $ cd web-service-gin
    
  3. 创建一个模块,您可以在其中管理依赖项.

    运行go mod init 命令,为其指定代码所在模块的路径.

    $ go mod init example/web-service-gin
    go: creating new go.mod: module example/web-service-gin
    

    此命令会创建一个 go.mod 文件,您添加的依赖项将在其中列出以进行跟踪。有关使用模块路径命名模块的更多信息,请参阅 管理依赖项.

接下来,您将设计用于处理数据的数据结构.

创建数据

为了使本教程简单,您将数据存储在内存中。更典型的 API 将与数据库交互.

请注意,将数据存储在内存中意味着每次停止服务器时专辑集都会丢失,然后在启动时重新创建.

写代码

  1. 使用您的文本编辑器,在 web-service 目录中创建一个名为 main.go 的文件。您将在此文件中编写 Go 代码.

  2. 在 main.go 文件的顶部,粘贴以下包声明.

    package main
    

    独立程序(而不是库)始终位于包main中.

  3. 在包声明下方,粘贴以下album 结构声明 。您将使用它在内存中存储专辑数据.

    结构标记,例如json:"artist"指定当结构的内容序列化为 JSON 时字段的名称应该是什么。如果没有它们,JSON 将使用结构体的大写字段名称——这种样式在 JSON 中并不常见.

    // album 表示有关专辑的数据.
    type album struct {
        ID     string  `json:"id"`
        Title  string  `json:"title"`
        Artist string  `json:"artist"`
        Price  float64 `json:"price"`
    }
    
  4. 在您刚刚添加的结构声明下方,粘贴以下album结构片段, 其中包含您将用于启动的数据.

    // 专辑切片以填充专辑数据记录.
    var albums = []album{
        {ID: "1", Title: "Blue Train", Artist: "John Coltrane", Price: 56.99},
        {ID: "2", Title: "Jeru", Artist: "Gerry Mulligan", Price: 17.99},
        {ID: "3", Title: "Sarah Vaughan and Clifford Brown", Artist: "Sarah Vaughan", Price: 39.99},
    }
    

接下来,您将编写代码来实现您的第一个端点.

编写一个处理程序来返回所有项目

当客户端在 GET /albums上发出请求时,您希望以 JSON 格式返回所有专辑。

为此,您将编写以下内容:

请注意,这与它们在运行时的执行方式相反,但首先要添加依赖项,然后添加依赖于它们的代码。

写代码

  1. 在上一节中添加的结构体代码下,粘贴以下代码以获取专辑列表.

    getAlbums函数从album结构切片创建 JSON ,将 JSON 写入响应。

    // getAlbums 以 JSON 格式响应所有专辑的列表.
    func getAlbums(c *gin.Context) {
        c.IndentedJSON(http.StatusOK, albums)
    }
    

    在此代码中,您:

    • 编写一个带gin.Context 参数的 getAlbums函数 。请注意,您可以为该函数指定任何名称——Gin 和 Go 都不需要特定的函数名称格式.

      gin.Context是Gin最重要的部分。它携带请求详细信息、验证和序列化 JSON 等。(尽管名称相似,但这与 Go 的内置context 包不同。)

    • 调用Context.IndentedJSON 将结构序列化为 JSON 并将其添加到响应中。

      该函数的第一个参数是要发送到客户端的 HTTP 状态代码。在这里,您将从 net/http 包传递 StatusOK 常量,以指示 200 OK

      请注意,您可以将 Context.IndentedJSON 替换为对 Context.JSON 的调用。在实践中,缩进形式在调试时更容易使用,并且大小差异通常很小。

  2. 在 main.go 的顶部附近,就在albums切片声明的下方,粘贴下面的代码以将处理程序函数分配给端点路径。

    这会建立一个关联,在该关联中getAlbums处理对/albums端点路径的请求。

    func main() {
        router := gin.Default()
        router.GET("/albums", getAlbums)
    
        router.Run("localhost:8080")
    }
    

    在此代码中,您:

    • 使用Default初始化 Gin 路由器.

    • 使用GET 函数将GET HTTP 方法和/albums路径与处理程序函数相关联.

      请注意,您正在传递 getAlbums 函数的 名称 。这与传递函数的 结果 不同,后者可以通过传递 getAlbums() 来完成(请注意括号).

    • 使用Run函数将路由器连接到一个http.Server并启动服务器。

  3. 在 main.go 的顶部附近,就在包声明的下方,导入支持刚刚编写的代码所需的包.

    第一行代码应该是这样的:

    package main
    
    import (
        "net/http"
    
        "github.com/gin-gonic/gin"
    )
    
  4. 保存 main.go.

运行代码

  1. 开始将 Gin 模块作为依赖项进行跟踪.

    在命令行中,使用go get 将 github.com/gin-gonic/gin 模块添加为您的模块的依赖项。使用点参数表示“获取当前目录中代码的依赖项”.

    $ go get .
    go get: added github.com/gin-gonic/gin v1.7.2
    

    Go 解析并下载此依赖项,以满足您在上一步中添加的 import声明.

  2. 从包含 main.go 的目录中的命令行,运行代码。使用点参数表示“在当前目录中运行代码。”

    $ go run .
    

    代码运行后,您就有了一个正在运行的 HTTP 服务器,您可以向其发送请求.

  3. 在新的命令行窗口中,用 curl向正在运行的 Web 服务发出请求。

    $ curl http://localhost:8080/albums
    

    该命令应显示您为服务填充的数据.

    [
            {
                    "id": "1",
                    "title": "Blue Train",
                    "artist": "John Coltrane",
                    "price": 56.99
            },
            {
                    "id": "2",
                    "title": "Jeru",
                    "artist": "Gerry Mulligan",
                    "price": 17.99
            },
            {
                    "id": "3",
                    "title": "Sarah Vaughan and Clifford Brown",
                    "artist": "Sarah Vaughan",
                    "price": 39.99
            }
    ]
    

您已经启动了一个 API!在下一部分中,您将使用代码创建另一个端点来处理POST添加项目的请求。

编写处理程序以添加新项目

当客户端在 /albums 发出 POST 请求时,您希望将请求正文中所述的专辑添加到现有专辑数据中。

为此,您将编写以下内容:

写代码

  1. 添加代码以将专辑数据添加到专辑列表中.

    import语句之后的某处粘贴以下代码。(文件的末尾是这段代码的好地方,但 Go 不强制你声明函数的顺序。)

    // postAlbums 从请求体中收到的JSON中添加一个专辑 .
    func postAlbums(c *gin.Context) {
        var newAlbum album
    
        // 调用 BindJSON 将收到的 JSON 绑定到
        // newAlbum.
        if err := c.BindJSON(&newAlbum); err != nil {
            return
        }
    
        // 将新专辑添加到切片.
        albums = append(albums, newAlbum)
        c.IndentedJSON(http.StatusCreated, newAlbum)
    }
    

    在此代码中,您:

    • 使用 Context.BindJSON 将请求正文绑定到 newAlbum.
    • 将从 JSON 初始化的 album 结构追加到 album 切片.
    • 向响应添加 201 状态代码,以及表示您添加的专辑的 JSON.
  2. 更改您的main 函数,使其包含 router.POST函数,如下所示.

    func main() {
        router := gin.Default()
        router.GET("/albums", getAlbums)
        router.POST("/albums", postAlbums)
    
        router.Run("localhost:8080")
    }
    

    在此代码中,您:

    • /albums 路径上的 POST 方法与 postAlbums 函数相关联.

      使用 Gin,您可以将处理程序与 HTTP 方法和路径组合相关联。通过这种方式,您可以根据客户端使用的方法单独路由发送到单个路径的请求.

运行代码

  1. 如果服务器从上一节开始仍在运行,请停止它.

  2. 从包含 main.go 的目录中的命令行,运行代码.

    $ go run .
    
  3. 从不同的命令行窗口,用于curl向正在运行的 Web 服务发出请求。

    $ curl http://localhost:8080/albums \
        --include \
        --header "Content-Type: application/json" \
        --request "POST" \
        --data '{"id": "4","title": "The Modern Sound of Betty Carter","artist": "Betty Carter","price": 49.99}'
    

    该命令应显示添加专辑的headers和 JSON.

    HTTP/1.1 201 Created
    Content-Type: application/json; charset=utf-8
    Date: Wed, 02 Jun 2021 00:34:12 GMT
    Content-Length: 116
    
    {
        "id": "4",
        "title": "The Modern Sound of Betty Carter",
        "artist": "Betty Carter",
        "price": 49.99
    }
    
  4. 与上一节一样,使用curl检索专辑的完整列表,您可以使用它来确认新专辑已添加。

    $ curl http://localhost:8080/albums \
        --header "Content-Type: application/json" \
        --request "GET"
    

    该命令应显示专辑列表.

    [
            {
                    "id": "1",
                    "title": "Blue Train",
                    "artist": "John Coltrane",
                    "price": 56.99
            },
            {
                    "id": "2",
                    "title": "Jeru",
                    "artist": "Gerry Mulligan",
                    "price": 17.99
            },
            {
                    "id": "3",
                    "title": "Sarah Vaughan and Clifford Brown",
                    "artist": "Sarah Vaughan",
                    "price": 39.99
            },
            {
                    "id": "4",
                    "title": "The Modern Sound of Betty Carter",
                    "artist": "Betty Carter",
                    "price": 49.99
            }
    ]
    

在下一节中,您将添加代码来处理特定项的 GET

编写处理程序以返回特定项目

当客户端向 GET /albums/[id] 发出请求时,您希望返回 ID 与 id path 参数匹配的专辑。

为此,您将:

写代码

  1. 在上一节中添加的 postAlbums 函数下,粘贴以下代码以检索特定专辑。

    getAlbumByID 函数将提取请求路径中的 ID,然后找到匹配的专辑。

    // getAlbumByID 查找 ID 值与客户端发送的 id 
    // 参数匹配的专辑,然后返回该专辑作为响应.
    func getAlbumByID(c *gin.Context) {
        id := c.Param("id")
    
        // 循环浏览专辑列表,查找
        // ID 值与参数匹配的专辑.
        for _, a := range albums {
            if a.ID == id {
                c.IndentedJSON(http.StatusOK, a)
                return
            }
        }
        c.IndentedJSON(http.StatusNotFound, gin.H{"message": "album not found"})
    }
    

    在此代码中,您:

    • 使用 Context.Param从 URL 中检索 id path 参数。将此处理程序映射到路径时,将在路径中包含参数的占位符.

    • 循环访问切片中的album结构,查找其 ID 字段值与 id 参数值匹配的结构。如果找到,则将该 album 结构序列化为 JSON,并将其作为响应返回,并带有200 OK HTTP 状态码.

      如上所述,现实世界的服务可能会使用数据库查询来执行此查找.

    • 如果专辑没有找到则返回一个带http.StatusNotFound的HTTP404错误.

  2. 最后,更改您的main使其包含对router.GET 的新调用,路径现在为/albums/:id,如以下示例所示。

    func main() {
        router := gin.Default()
        router.GET("/albums", getAlbums)
        router.GET("/albums/:id", getAlbumByID)
        router.POST("/albums", postAlbums)
    
        router.Run("localhost:8080")
    }
    

    在此代码中,您:

    • 在 Gin 中,路径中项目前面的冒号表示该项目是路径参数.

运行代码

  1. 如果服务器从上一节开始仍在运行,请停止它.

  2. 从包含 main.go 的目录中的命令行,运行代码以启动服务器.

    $ go run .
    
  3. 从不同的命令行窗口,用 curl向正在运行的 Web 服务发出请求.

    $ curl http://localhost:8080/albums/2
    

    该命令应显示您使用其 ID 的专辑的 JSON。如果未找到专辑,您将收到带有错误消息的 JSON.

    {
            "id": "2",
            "title": "Jeru",
            "artist": "Gerry Mulligan",
            "price": 17.99
    }
    

总结

恭喜!您刚刚使用 Go 和 Gin 编写了一个简单的 RESTful Web 服务.

建议的下一个主题:

完整代码

本节包含您使用本教程构建的应用程序的代码.

package main

import (
    "net/http"

    "github.com/gin-gonic/gin"
)

// album 表示有关专辑的数据.
type album struct {
    ID     string  `json:"id"`
    Title  string  `json:"title"`
    Artist string  `json:"artist"`
    Price  float64 `json:"price"`
}

// 专辑切片以填充专辑数据记录.
var albums = []album{
    {ID: "1", Title: "Blue Train", Artist: "John Coltrane", Price: 56.99},
    {ID: "2", Title: "Jeru", Artist: "Gerry Mulligan", Price: 17.99},
    {ID: "3", Title: "Sarah Vaughan and Clifford Brown", Artist: "Sarah Vaughan", Price: 39.99},
}

func main() {
    router := gin.Default()
    router.GET("/albums", getAlbums)
    router.GET("/albums/:id", getAlbumByID)
    router.POST("/albums", postAlbums)

    router.Run("localhost:8080")
}

// getAlbums 以 JSON 格式响应所有专辑的列表.
func getAlbums(c *gin.Context) {
    c.IndentedJSON(http.StatusOK, albums)
}

// postAlbums 从请求体中收到的JSON中添加一个专辑.
func postAlbums(c *gin.Context) {
    var newAlbum album

    // 调用 BindJSON 将收到的 JSON 绑定到
    // newAlbum.
    if err := c.BindJSON(&newAlbum); err != nil {
        return
    }

    // 将新专辑添加到切片.
    albums = append(albums, newAlbum)
    c.IndentedJSON(http.StatusCreated, newAlbum)
}

// getAlbumByID 查找 ID 值与客户端发送的 id
// 参数匹配的专辑,然后返回该专辑作为响应.
func getAlbumByID(c *gin.Context) {
    id := c.Param("id")

    // 循环浏览专辑列表,查找
    // ID 值与参数匹配的专辑.
    for _, a := range albums {
        if a.ID == id {
            c.IndentedJSON(http.StatusOK, a)
            return
        }
    }
    c.IndentedJSON(http.StatusNotFound, gin.H{"message": "album not found"})
}