使用 Go 和 Gin 开发 RESTful API
本教程介绍了使用 Go 和 Gin Web 框架 (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 Insider build 17063 及更高版本中。对于较早的 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包中。 -
在包声明下方,粘贴以下
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"` } -
在你刚刚添加的结构体声明下方,粘贴以下包含专辑数据的结构体切片。
// 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 形式返回所有专辑。
为此,你将编写以下内容:
- 准备响应的逻辑
- 将请求路径映射到你的逻辑的代码
注意,这与它们在运行时执行的顺序相反,但你先添加依赖项,然后添加依赖它们的代码。
编写代码
-
在你上一节添加的结构体代码下方,粘贴以下代码以获取专辑列表。
此
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。在实践中,缩进形式在调试时更容易使用,且大小差异通常很小。
-
-
在
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.2Go 解析并下载了这个依赖项,以满足你在上一步中添加的
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结构体追加到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 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 } -
如上一节所述,使用
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 路径参数匹配的专辑。
为此,你将:
- 添加检索请求专辑的逻辑
- 将路径映射到逻辑
编写代码
-
在你上一节添加的
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 OKHTTP 代码的响应。如上所述,真实世界的服务可能会使用数据库查询来执行此查找。
-
如果未找到专辑,则使用
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") }在这段代码中,你:
- 将
/albums/:id路径与getAlbumByID函数关联。在 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 }
Conclusion
Congratulations! You've just used Go and Gin to write a simple RESTful web service.
Suggested next topics:
- If you're new to Go, you'll find useful best practices described in Effective Go and How to write Go code.
- The Go Tour is a great step-by-step introduction to Go fundamentals.
- For more about Gin, see the Gin Web Framework package documentation or the Gin Web Framework docs.
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"})
}