Go语言基地

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

On this page