教程:使用 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. 编写一个处理程序以返回所有项目。
  5. 编写一个处理程序以添加一个新项目。
  6. 编写一个处理程序以返回一个特定项目。

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

要尝试以在 Google Cloud Shell 中完成的交互式教程的形式进行此操作,请点击下面的按钮。

Open in Cloud Shell

先决条件

设计 API 端点

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

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

以下是你将在本教程中创建的端点。

/albums

/albums/:id

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

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

首先,为你要编写的代码创建一个项目。

  1. 打开一个命令提示符并切换到你的主目录。

    在 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 represents data about a record album.
    type album struct {
        ID     string  `json:"id"`
        Title  string  `json:"title"`
        Artist string  `json:"artist"`
        Price  float64 `json:"price"`
    }
    
  4. 在您刚刚添加的结构声明下方,粘贴以下包含您将用来开始的 album 结构的切片。

    // albums slice to seed record album data.
    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 responds with the list of all albums as JSON.
    func getAlbums(c *gin.Context) {
        c.IndentedJSON(http.StatusOK, albums)
    }
    

    在此代码中,您

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

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

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

      该函数的第一个参数是要发送给客户端的 HTTP 状态代码。在此处,您传递的是来自 net/http 包的 StatusOK 常量,以指示 200 OK

      请注意,您可以用对 Context.JSON 的调用替换 Context.IndentedJSON 以发送更紧凑的 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 https://127.0.0.1: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 adds an album from JSON received in the request body.
    func postAlbums(c *gin.Context) {
        var newAlbum album
    
        // Call BindJSON to bind the received JSON to
        // newAlbum.
        if err := c.BindJSON(&newAlbum); err != nil {
            return
        }
    
        // Add the new album to the slice.
        albums = append(albums, newAlbum)
        c.IndentedJSON(http.StatusCreated, newAlbum)
    }
    

    在此代码中,您

    • 使用 Context.BindJSON 将请求正文绑定到 newAlbum
    • 将从 JSON 初始化的 album 结构追加到 albums 切片。
    • 向响应中添加 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 https://127.0.0.1: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}'
    

    该命令应显示已添加专辑的标头和 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 https://127.0.0.1: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 路径参数匹配的专辑。

为此,您将

编写代码

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

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

    // getAlbumByID locates the album whose ID value matches the id
    // parameter sent by the client, then returns that album as a response.
    func getAlbumByID(c *gin.Context) {
        id := c.Param("id")
    
        // Loop over the list of albums, looking for
        // an album whose ID value matches the parameter.
        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 路径参数。将此处理程序映射到路径时,您将在路径中包含一个占位符以供参数使用。

    • 遍历切片中的 album 结构,查找其中 ID 字段值与 id 参数值匹配的一个。如果找到,您将把该 album 结构序列化为 JSON,并以 200 OK HTTP 代码作为响应返回。

      如上所述,实际服务可能使用数据库查询来执行此查找。

    • 如果未找到专辑,请返回 HTTP 404 错误,其中包含 http.StatusNotFound

  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")
    }
    

    在此代码中,您

    • /albums/:id 路径与 getAlbumByID 函数关联。在 Gin 中,路径中项目前面的冒号表示该项目是一个路径参数。

运行代码

  1. 如果服务器仍在从上一部分运行,请停止它。

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

    $ go run .
    
  3. 从另一个命令行窗口中,使用 curl 向正在运行的 Web 服务发出请求。

    $ curl https://127.0.0.1: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 represents data about a record album.
type album struct {
    ID     string  `json:"id"`
    Title  string  `json:"title"`
    Artist string  `json:"artist"`
    Price  float64 `json:"price"`
}

// albums slice to seed record album data.
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 responds with the list of all albums as JSON.
func getAlbums(c *gin.Context) {
    c.IndentedJSON(http.StatusOK, albums)
}

// postAlbums adds an album from JSON received in the request body.
func postAlbums(c *gin.Context) {
    var newAlbum album

    // Call BindJSON to bind the received JSON to
    // newAlbum.
    if err := c.BindJSON(&newAlbum); err != nil {
        return
    }

    // Add the new album to the slice.
    albums = append(albums, newAlbum)
    c.IndentedJSON(http.StatusCreated, newAlbum)
}

// getAlbumByID locates the album whose ID value matches the id
// parameter sent by the client, then returns that album as a response.
func getAlbumByID(c *gin.Context) {
    id := c.Param("id")

    // Loop through the list of albums, looking for
    // an album whose ID value matches the parameter.
    for _, a := range albums {
        if a.ID == id {
            c.IndentedJSON(http.StatusOK, a)
            return
        }
    }
    c.IndentedJSON(http.StatusNotFound, gin.H{"message": "album not found"})
}