教程:使用 Go 和 Gin 开发 RESTful API
本教程介绍了使用 Go 和 Gin Web 框架 (Gin)编写 RESTful Web 服务 API 的基础知识.
如果您对 Go 及其工具有基本的了解,您将从本教程中获得最大收益。如果这是您第一次接触 Go,请参阅 教程:Go 入门以获取 快速介绍。
Gin 简化了许多与构建 Web 应用程序(包括 Web 服务)相关的编码任务。在本教程中,您将使用 Gin 路由请求、检索请求详细信息以及组织 JSON 以进行响应。
在本教程中,您将构建一个具有两个端点的 RESTful API 服务器。您的示例项目将是有关复古爵士乐唱片的数据存储库.
本教程包括以下部分:
- 设计 API 端点.
- 为您的代码创建一个文件夹.
- 创建数据.
- 编写一个处理程序来返回所有项目(item).
- 编写一个处理程序来添加一个新项目.
- 编写一个处理程序来返回一个特定的项目.
注意: 有关其他教程,请参阅 教程.
要尝试将此作为您在 Google Cloud Shell 中完成的交互式教程,请点击下面的按钮.
先决条件
- Go 1.16 或更高版本的安装. 有关安装说明,请参阅 安装 Go.
- 一种编辑代码的工具. 您拥有的任何文本编辑器都可以正常工作.
- 命令终端. Go 适用于 Linux 和 Mac 上的任何终端,以及 Windows 中的 PowerShell 或 cmd.
- curl 工具. 在 Linux 和 Mac 上,这应该已经安装了. 在 Windows 上,它包含在 Windows 10 Insider build 17063 及更高版本中。对于较早的 Windows 版本,您可能需要安装它。有关更多信息,请参阅 Tar 和 Curl 来到 Windows.
设计 API 端点
您将构建一个 API,该 API 提供对销售黑胶唱片老式唱片的商店的访问。因此,您需要提供端点,客户端可以通过这些端点为用户获取和添加专辑.
在开发 API 时,您通常从设计端点开始。如果端点易于理解,您的 API 用户将获得更多成功.
以下是您将在本教程中创建的端点.
/albums
GET
– 获取所有专辑的列表,以 JSON 形式返回.POST
– 从以 JSON 形式发送的请求数据中添加新专辑.
/albums/:id
GET
– 通过 ID 获取专辑,以 JSON 形式返回专辑数据.
接下来,您将为您的代码创建一个文件夹.
为您的代码创建一个文件夹
首先,为您将编写的代码创建一个项目(project).
-
打开命令提示符并切换到您的home目录.
在 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
中. -
在包声明下方,粘贴以下
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"` }
-
在您刚刚添加的结构声明下方,粘贴以下
album
结构片段, 其中包含您将用于启动的数据.// 专辑切片以填充专辑数据记录. 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 以 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
的调用。在实践中,缩进形式在调试时更容易使用,并且大小差异通常很小。
-
-
在 main.go 的顶部附近,就在
albums
切片声明的下方,粘贴下面的代码以将处理程序函数分配给端点路径。这会建立一个关联,在该关联中
getAlbums
处理对/albums
端点路径的请求。func main() { router := gin.Default() router.GET("/albums", getAlbums) router.Run("localhost:8080") }
在此代码中,您:
-
在 main.go 的顶部附近,就在包声明的下方,导入支持刚刚编写的代码所需的包.
第一行代码应该是这样的:
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 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
请求路由到您的逻辑的一些代码.
写代码
-
添加代码以将专辑数据添加到专辑列表中.
在
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
结构追加到album
切片. - 向响应添加
201
状态代码,以及表示您添加的专辑的 JSON.
- 使用
-
更改您的
main
函数,使其包含router.POST
函数,如下所示.func main() { router := gin.Default() router.GET("/albums", getAlbums) router.POST("/albums", postAlbums) router.Run("localhost:8080") }
在此代码中,您:
-
将
/albums
路径上的POST
方法与postAlbums
函数相关联.使用 Gin,您可以将处理程序与 HTTP 方法和路径组合相关联。通过这种方式,您可以根据客户端使用的方法单独路由发送到单个路径的请求.
-
运行代码
-
如果服务器从上一节开始仍在运行,请停止它.
-
从包含 main.go 的目录中的命令行,运行代码.
$ go run .
-
从不同的命令行窗口,用于
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}'
该命令应显示添加专辑的headers和 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 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
path 参数匹配的专辑。
为此,您将:
- 添加逻辑以检索请求的专辑.
- 将路径映射到逻辑.
写代码
-
在上一节中添加的
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
path 参数。将此处理程序映射到路径时,将在路径中包含参数的占位符. -
循环访问切片中的
album
结构,查找其ID
字段值与id
参数值匹配的结构。如果找到,则将该album
结构序列化为 JSON,并将其作为响应返回,并带有200 OK
HTTP 状态码.如上所述,现实世界的服务可能会使用数据库查询来执行此查找.
-
如果专辑没有找到则返回一个带
http.StatusNotFound
的HTTP404
错误.
-
-
最后,更改您的
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") }
在此代码中,您:
- 在 Gin 中,路径中项目前面的冒号表示该项目是路径参数.
运行代码
-
如果服务器从上一节开始仍在运行,请停止它.
-
从包含 main.go 的目录中的命令行,运行代码以启动服务器.
$ go run .
-
从不同的命令行窗口,用
curl
向正在运行的 Web 服务发出请求.$ curl http://localhost:8080/albums/2
该命令应显示您使用其 ID 的专辑的 JSON。如果未找到专辑,您将收到带有错误消息的 JSON.
{ "id": "2", "title": "Jeru", "artist": "Gerry Mulligan", "price": 17.99 }
总结
恭喜!您刚刚使用 Go 和 Gin 编写了一个简单的 RESTful Web 服务.
建议的下一个主题:
- 如果您是 Go 新手,您会在Effective Go和如何编写 Go 代码中找到有用的最佳实践 。
- Go 边学边练是对Go基础知识的一个很好的分步介绍。
- 有关 Gin 的更多信息,请参阅 Gin Web 框架包文档 或Gin Web 框架文档。
完整代码
本节包含您使用本教程构建的应用程序的代码.
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"`
}
// 专辑切片以填充专辑数据记录.
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"})
}