教程:使用 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
函数将GET
HTTP 方法和/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.2
Go 已解析并下载了此依赖项,以满足您在上一步中添加的
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 OK
HTTP 代码作为响应返回。如上所述,真实世界的服务可能使用数据库查询来执行此查找。
-
如果未找到专辑,则返回 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"})
}