使用 Go 和 Gin 开发 RESTful API

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


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

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

本教程包括以下部分:

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

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

Open in Google Cloud Shell

本教程需要 Go 1.16 或更高版本。

1. 设计 API 端点

您将构建一个 API,该 API 可让您访问出售老式黑胶唱片的商店。因此,您需要提供端点(endpoints),客户端可以通过这些端点为用户获取和添加相册。

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

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

/albums

  • GET – 获取所有专辑的列表,以 JSON 形式返回。
  • POST – 从作为 JSON 发送的请求数据中添加新专辑。

/albums/:id

  • GET – 通过 ID 获取专辑,将专辑数据作为 JSON 返回。

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

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

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

  1. 打开命令提示符并切换到您的主目录。
  2. 创建一个名为 web-service-gin 的目录。

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

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

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

3. 创建数据

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

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

3.1. 编写代码

  1. 在 web-service 目录中创建一个名为 main.go 的文件。您将在此文件中编写您的 Go 代码。
  2. 进入 main.go,在文件顶部,粘贴以下包声明。

    package main
    

    独立程序(与库相反)始终位于 package 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},
    }
    

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

4. 查询所有项目

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

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

  • 准备响应的逻辑
  • 将请求路径映射到您的逻辑的代码

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

4.1. 编写代码

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

    在此代码中,您:

    • 编写一个带 gin.Context 参数的函数getAlbums。请注意,你可以给这个函数起任何名字——Gin 和 Go 都不需要特定的函数名格式。

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

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

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

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

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

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

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

    在此代码中,您:

    • 使用 Default 初始化 Gin router 。
    • 使用该 GET 函数将 GET HTTP 方法和/albums路径与处理函数相关联。

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

    • 使用 Run 函数将 router 连接到http.Server并启动server。

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

    第一行代码应如下所示:

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

4.2. 运行代码

  1. 开始跟踪 Gin 模块作为依赖项。

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

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

    解决并下载此依赖项以满足您在上一步中添加的 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添加项目的请求。

5. 添加一个新项目

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

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

  • 将新专辑添加到现有列表的逻辑。
  • POST请求路由到您的逻辑的一些代码。

5.1. 编写代码

  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 切片。
    • 向响应response添加状态代码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 方法和路径组合 相关联。这样,您可以根据客户端使用的方法将发送到单个路径的请求单独路由。

5.2. 运行代码

  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}'
    

    该命令应显示添加专辑的标题和 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特定项目的请求。

6. 查询特定的项目

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

为此,您将:

  • 添加逻辑以检索请求的相册。
  • 将路径映射到逻辑。

6.1. 编写代码

  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.StatusNotFound 即 HTTP 404错误。

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

    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 中,路径中项目前面的冒号表示该项目是路径参数。

6.2. 运行代码

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

参考

https://go.dev/doc/tutorial/web-service-gin