本文翻译自《Tutorial: Developing a RESTful API with Go and Gin》。
目录
本教程介绍了使用Go和Web开发框架Gin编写RESTful web服务API的基本知识。
如果你对Go及其工具有基本的熟悉,你将从本教程中获得最大的收获。如果这是你第一次接触Go,请先参阅教程:Go快速入门。
Gin简化了许多与构建web应用程序(包括web服务)相关的编程任务。在本教程中,你将使用Gin来路由请求、检索请求详细信息,并为发送JSON响应。
在本教程中,你将构建一个具有两个端点的RESTful API服务器。你的示例项目将是一个关于老式爵士乐唱片的数据存储库。
本教程包括以下部分:
1 设计API端点。
2 为代码创建一个文件夹。
3 创建数据。
4 编写一个处理程序以返回所有条目。
5 编写一个处理程序来添加一个新条目。
6 编写一个处理程序以返回一个特定条目。
先决条件
- 安装Go 1.16或更高版本。有关安装说明,请参阅安装Go。
- 用于编辑代码的工具。你拥有的任何文本编辑器都可以正常工作。
- 一种命令行终端。Go在Linux和Mac上使用任何终端都能很好地工作,以及Windows中的PowerShell或CMD。
- curl程序。在Linux和Mac上,应该已经安装了。在Windows上,它包含在Windows 10 Insider版本17063及更高版本中。对于早期的Windows版本,你可能需要安装它。有关更多信息,请参阅Tar and Curl Come to Windows。
设计API端点
你将建立一个API,提供对一家出售老式黑胶唱片的商店的访问。因此,你需要提供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-gin
目录中创建一个名为main.go的文件。你将在该文件中编写Go代码。
2 在文件顶部的main.go中,粘贴以下包声明。
package main
独立可运行的程序(与库相对)始终位于main包中。
3 在包声明下面,粘贴album
结构体的以下声明。你将使用它将相册数据存储在内存中。
代码中的结构体标记(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},
}
接下来,你将编写代码来实现你的第一个API端点。
编写处理程序以返回所有条目
当客户端在GET /albums
上发出请求时,你希望以JSON的形式返回所有的相册信息。
为此,你将编写以下代码:
- 响应的逻辑
- 将请求路径映射到响应的逻辑
但你首先要添加依赖项,然后添加依赖于它们的代码。
编写代码
1 在上一节中添加的结构体代码下面,粘贴以下代码以获得相册信息的列表。
这个getAlbums
函数从相册结构体的切片albums
创建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.IndetedJSON
函数将结构体序列化为JSON并将其添加到响应中。
该函数的第一个参数是要发送到客户端的HTTP状态代码。在这里,你传递net/http
包的StatusOK
常量,表示200 OK这一HTTP状态代码。
请注意,你可以将Context.IndetedJSON
函数替换为Context.JSON
函数,以发送更紧凑的JSON数据。在实践中,缩进形式的JSON数据在调试时更具可读性,而且也不会比紧凑的JSON数据大很多。
2 在main.go顶部附近的albums
切片声明下方,粘贴下面的代码,将处函数分配给API端点。
这设置了一个关联,getAlbums
函数处理对/albums
路径的请求。
func main() {
router := gin.Default()
router.GET("/albums", getAlbums)
router.Run("localhost:8080")
}
在此代码中,你:
请注意,你传递的是getAlbums
函数的名称。这与传递函数的结果不同,传递函数的结果是传递getAlbums()
(注意括号)。
- 使用
Run
函数将路由器关联到一个http服务器并启动服务器。
3 在main.go的顶部,就在包声明的下方,导入用到的包。
第一行代码应该如下所示:
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
4 保存main.go文件。
运行代码
1 引入Gin模块作为依赖项。 在命令行中,使用go get
添加github.com/gin-gonic/gin
模块作为你的example/web-service-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
将会返回以下JSON格式的数据:
[
{
"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!在下一节中,你将使用代码创建另一个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
}
// 把newAlbum添加到albums列表里。
albums = append(albums, newAlbum)
c.IndentedJSON(http.StatusCreated, newAlbum)
}
在此代码中,你:
- 使用
Context.BindJSON
函数将请求正文绑定到newAlbum
结构体变量。 - 将从JSON数据转换得到的
album
结构体变量添加到albums
切片。 - 在响应中添加一个201状态代码,以及表示你成功创建一条新相册信息数据。
2 更改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方法和API路径相关联。通过这种方式,你可以根据客户端使用的HTTP方法将发送到某个API路径的请求单独路由到某个处理函数。
运行代码
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响应状态数据:
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
路径参数匹配的相册的信息。
为此,你将:
- 添加代码以检索请求的相册的信息数据。
- 将API路径映射到上述代码。
编写代码
1 在上一节中添加的postAlbums
函数下面,粘贴以下代码以检索特定的相册。
此getAlbumByID
函数将提取请求路径中的id
,然后查找匹配的相册的信息。
// getAlbumByID函数使用客户端发送过来的id参数定位并返回相册数据作为响应。
func getAlbumByID(c *gin.Context) {
id := c.Param("id")
// 循环遍历albums列表,查找匹配id参数的album。
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路径参数。将此处理程序映射到API路径时,将在API路径中包含id
参数的占位符。 - 循环遍历
albums
切片中的album
结构体变量,查找ID字段值与id
参数值匹配的那个结构体。如果找到了,则将该album
序列化为JSON,并将其作为响应返回,并返回一个200 OK的HTTP状态码。
如上所述,真实世界的服务可能会使用数据库查询来执行此查找。
- 如果找不到匹配的
album
,则返回HTTP.StatusNotFound
代表的HTTP 404错误。
2 最后,更改main
函数,添加一个router.GET
路由到getAlbumByID
函数,其中的API路径是/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
这个API路径与getAlbumByID
函数相关联。在Gin中,API路径中前面的冒号表示这个条目是一个路径参数。
运行代码
1 停止运行之前的程序。
2 在包含main.go的目录的命令行中,运行代码:
$ go run .
3 从另一个命令行窗口,使用curl
工具向正在运行的web服务发出请求:
$ curl http://localhost:8080/albums/2
该命令执行后应该显示你给出ID的相册信息的JSON数据:
{
"id": "2",
"title": "Jeru",
"artist": "Gerry Mulligan",
"price": 17.99
}
如果找不到对应的相册信息,你将获得带有错误消息的JSON。
结论
恭喜你刚刚使用Go和Gin编写了一个简单的RESTful web服务。
建议的下一个主题:
- 如果你是Go新手,你会在“Effective Go”和“如何编写Go代码”中找到有用的最佳实践。
- Go Tour是对Go基础知识的一个很好的逐步介绍。
- 有关Gin的更多信息,请参阅Gin WebFramework包的文档或Gin Webframework的文档。
全部代码
本节包含使用本教程构建的应用程序的全部代码:
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"})
}