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

本教程将介绍使用 Go 和 Gin Web Framework (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

先决条件

  • 安装 Go 1.16 或更高版本。 有关安装说明,请参阅 安装 Go
  • 一个代码编辑工具。 任何文本编辑器都可以。
  • 命令终端。 Go 在 Linux 和 Mac 上的任何终端以及 Windows 上的 PowerShell 或 cmd 中都能很好地工作。
  • curl 工具。 在 Linux 和 Mac 上,此工具应已预装。在 Windows 上,它包含在 Windows 10 内部版本 17063 及更高版本中。对于早期版本的 Windows,您可能需要安装它。更多信息,请参阅 Tar 和 Curl 在 Windows 上可用

设计 API 端点

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

开发 API 时,通常从设计端点开始。API 用户会更容易理解端点,从而提高成功率。

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

/albums

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

/albums/:id

  • GET – 按 ID 获取专辑,以 JSON 格式返回专辑数据。

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

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

首先,创建一个项目来存放您将编写的代码。

  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. 在 package 声明下方,粘贴以下 album 结构体的声明。您将使用它来在内存中存储专辑数据。

    Struct 标签,如 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)
    }
    

    在此代码中,您

    • 编写一个接受 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 切片声明下方,粘贴以下代码以将处理器函数分配给一个端点路径。

    这会建立一个关联,其中 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 声明下方,导入您将需要的包来支持您刚刚编写的代码。

    代码的第一行应如下所示:

    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://: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 请求以添加一个项。

编写一个添加新项的处理器

当客户端在 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")
    }
    

    在此代码中,您

    • POST 方法在 /albums 路径与 postAlbums 函数关联起来。

      使用 Gin,您可以将处理器与 HTTP 方法和路径的组合关联起来。通过这种方式,您可以根据客户端使用的方法,根据发送到单个路径的请求分别进行路由。

运行代码

  1. 如果服务器在上一个部分仍然运行,请将其停止。

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

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

    $ curl https://: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://: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://: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"})
}