• 简体中文
  • 使用 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 中完成的交互式教程,请点击下面的按钮。

    先决条件

    • Go 1.16 或更高版本的安装。 有关安装说明,请参阅 安装 Go
    • 一个用于编辑代码的工具。 任何你拥有的文本编辑器都可以正常工作。
    • 一个命令终端。 Go 在 Linux 和 Mac 上的任何终端上都能很好地工作,在 Windows 上的 PowerShell 或 cmd 上也是如此。
    • curl 工具。 在 Linux 和 Mac 上,这应该已经安装。在 Windows 上,它包含在 Windows 10 Insider build 17063 及更高版本中。对于较早的 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. 在包声明下方,粘贴以下 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. 在你刚刚添加的结构体声明下方,粘贴以下包含专辑数据的结构体切片。

      // albums 切片用于存储专辑数据。
      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)
      }

      在这段代码中,你:

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

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

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

        函数的第一个参数是你想发送给客户端的 HTTP 状态码。在这里,你传递了 StatusOK 常量从 net/http 包中,以指示 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()(注意括号)来实现。

      • 使用 Run 函数将路由器附加到 http.Server 并启动服务器。

    3. main.go 顶部附近,紧接在包声明下方,导入你刚编写代码所需的包。

      代码的前几行应如下所示:

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

    运行代码

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

      在命令行中,使用 go getgithub.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 请求时,你想将从请求正文中描述的专辑添加到现有专辑数据中。

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

    • 将新专辑添加到现有列表的逻辑
    • 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 结构体追加到 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 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 请求。

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

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

    为此,你将:

    • 添加检索请求专辑的逻辑
    • 将路径映射到逻辑

    编写代码

    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 路径参数。当你将此处理程序映射到路径时,将在路径中包含参数的占位符。

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

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

      • 如果未找到专辑,则使用 http.StatusNotFound 返回 HTTP 404 错误。

    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 http://localhost:8080/albums/2

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

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

    Conclusion

    Congratulations! You've just used Go and Gin to write a simple RESTful web service.

    Suggested next topics:

    Completed code

    This section contains the code for the application you build with this tutorial.

    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"})
    }
    ```## 结论
    
    恭喜!你刚刚使用 Go 和 Gin 编写了一个简单的 RESTful Web 服务。
    
    建议的后续主题:
    
    *   如果你是 Go 新手,你会在 [Effective Go](/en/docs/effective_go) 和
        [如何编写 Go 代码](https://go.dev/doc/code) 中发现有用的最佳实践。
    *   [Go 教程](https://go.dev/tour/) 是 Go 基础的逐步介绍。
    *   有关 Gin 的更多信息,请参阅 [Gin Web 框架包文档](https://pkg.go.dev/github.com/gin-gonic/gin)
        或 [Gin Web 框架文档](https://gin-gonic.com/en/docs/)。
    
    ## 完整代码
    
    本节包含你通过本教程构建的应用程序的代码。
    
    ```go
    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"`
    }
    
    // albums 切片用于存储专辑数据。
    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"})
    }