教程:使用 Go 和 Gin 开发 RESTful API
本教程将介绍使用 Go 和 Gin Web Framework (Gin) 编写 RESTful Web 服务 API 的基础知识。
如果您对 Go 及其工具链有基本的了解,将能从本教程中获得最大收益。如果您是第一次接触 Go,请参阅 教程:Go 入门 以快速了解。
Gin 简化了构建 Web 应用程序(包括 Web 服务)的许多编码任务。在本教程中,您将使用 Gin 来路由请求、检索请求详细信息以及为响应进行 JSON 编组。
在本教程中,您将构建一个具有两个端点的 RESTful API 服务器。您的示例项目将是一个关于老式爵士唱片数据的存储库。
本教程包含以下章节
- 设计 API 端点。
- 为您的代码创建一个文件夹。
- 创建数据。
- 编写一个返回所有项的处理器。
- 编写一个添加新项的处理器。
- 编写一个返回特定项的处理器。
注意: 有关其他教程,请参阅教程。
要以交互式教程的形式完成此操作,您可以在 Google 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 格式返回专辑数据。
接下来,您将创建一个文件夹来存放您的代码。
为您的代码创建一个文件夹
首先,创建一个项目来存放您将编写的代码。
- 
打开命令提示符并切换到您的主目录。 在 Linux 或 Mac 上 $ cd在 Windows 上 C:\> cd %HOMEPATH%
- 
使用命令提示符,创建一个名为 web-service-gin 的代码目录。 $ mkdir web-service-gin $ cd web-service-gin
- 
创建一个模块来管理依赖项。 运行 go mod init命令,并指定您的代码所在的模块路径。$ go mod init example/web-service-gin go: creating new go.mod: module example/web-service-gin此命令会创建一个 go.mod 文件,其中将列出您添加的依赖项以便跟踪。有关使用模块路径命名模块的更多信息,请参阅 管理依赖项。 
接下来,您将设计用于处理数据的结构体。
创建数据
为使教程保持简单,您将在内存中存储数据。更典型的 API 会与数据库进行交互。
请注意,将数据存储在内存中意味着每次停止服务器时,专辑集合都将丢失,并在启动时重新创建。
编写代码
- 
使用文本编辑器,在 web-service 目录中创建一个名为 main.go 的文件。您将在该文件中编写 Go 代码。 
- 
在 main.go 文件的顶部,粘贴以下包声明。 package main独立程序(与库相对)始终位于 `main` 包中。 
- 
在 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"` }
- 
在您刚刚添加的结构体声明下方,粘贴以下 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 格式返回所有专辑。
为此,您将编写以下内容
- 准备响应的逻辑
- 将请求路径映射到您的逻辑的代码
请注意,这与它们在运行时执行的顺序相反,但您是先添加依赖项,然后是依赖于这些依赖项的代码。
编写代码
- 
在上一节添加的结构体代码下方,粘贴以下代码以获取专辑列表。 此 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。实际上,缩进形式在调试时更容易使用,并且尺寸差异通常很小。
 
- 
- 
在 main.go 的顶部附近,紧随 albums切片声明下方,粘贴以下代码以将处理器函数分配给一个端点路径。这会建立一个关联,其中 getAlbums处理对/albums端点路径的请求。func main() { router := gin.Default() router.GET("/albums", getAlbums) router.Run("localhost:8080") }在此代码中,您 - 
使用 Default初始化一个 Gin 路由器。
- 
使用 GET函数将GETHTTP 方法和/albums路径与处理器函数关联起来。请注意,您传递的是 getAlbums函数的名称。这不同于传递函数的结果,后者您会通过传递getAlbums()(注意括号) 来实现。
- 
使用 Run函数将路由器附加到http.Server并启动服务器。
 
- 
- 
在 main.go 的顶部附近,紧随 package 声明下方,导入您将需要的包来支持您刚刚编写的代码。 代码的第一行应如下所示: package main import ( "net/http" "github.com/gin-gonic/gin" )
- 
保存 main.go。 
运行代码
- 
开始跟踪 Gin 模块作为依赖项。 在命令行中,使用 go get命令将 github.com/gin-gonic/gin 模块添加为您的模块的依赖项。使用点参数表示“获取当前目录中代码的依赖项”。$ go get . go get: added github.com/gin-gonic/gin v1.7.2Go 已解析并下载了此依赖项,以满足您在上一步中添加的 import声明。
- 
从包含 main.go 的目录的命令行中运行代码。使用点参数表示“运行当前目录中的代码”。 $ go run .代码运行后,您就拥有了一个正在运行的 HTTP 服务器,您可以向其发送请求。 
- 
从一个新的命令行窗口,使用 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请求路由到您的逻辑。
编写代码
- 
添加代码以将专辑数据添加到专辑列表中。 在 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。
 
- 使用 
- 
修改您的 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 方法和路径的组合关联起来。通过这种方式,您可以根据客户端使用的方法,根据发送到单个路径的请求分别进行路由。 
 
- 
运行代码
- 
如果服务器在上一个部分仍然运行,请将其停止。 
- 
在包含 main.go 的目录的命令行中,运行代码。 $ go run .
- 
从另一个命令行窗口,使用 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 }
- 
与上一节一样,使用 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 路径参数匹配的专辑。
为此,您将
- 添加检索所请求专辑的逻辑。
- 将路径映射到逻辑。
编写代码
- 
在上一节添加的 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 OKHTTP 代码作为响应返回。如上所述,真实世界的服务可能使用数据库查询来执行此查找。 
- 
如果未找到专辑,则返回 HTTP 404错误,使用http.StatusNotFound。
 
- 
- 
最后,修改您的 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 中,路径中项目前面的冒号表示该项目是路径参数。
 
- 将 
运行代码
- 
如果服务器在上一个部分仍然运行,请将其停止。 
- 
从包含 main.go 的目录的命令行中运行代码以启动服务器。 $ go run .
- 
从另一个命令行窗口,使用 curl向您正在运行的 Web 服务发出请求。$ curl https://:8080/albums/2命令应显示您使用的 ID 专辑的 JSON。如果未找到专辑,您将收到包含错误消息的 JSON。 { "id": "2", "title": "Jeru", "artist": "Gerry Mulligan", "price": 17.99 }
结论
恭喜!您刚刚使用 Go 和 Gin 编写了一个简单的 RESTful Web 服务。
建议的后续主题
- 如果您是 Go 新手,您会在 Effective Go 和 How to write Go code 中找到有用的最佳实践。
- Go 教程是 Go 基础知识的绝佳循序渐进介绍。
- 有关 Gin 的更多信息,请参阅 Gin Web Framework 包文档或 Gin Web Framework 文档。
完成的代码
本节包含您通过本教程构建的应用程序的代码。
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"})
}
