教程:使用Go和Gin开发RESTful API

本文翻译自《Tutorial: Developing a RESTful API with Go and Gin》。

目录

先决条件

设计API端点

为代码创建文件夹

创建数据

编写处理程序以返回所有条目

编写处理程序以添加一个新条目

编写处理程序以返回一个特定条目

结论

全部代码

本教程介绍了使用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包不同。)

该函数的第一个参数是要发送到客户端的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")
}

在此代码中,你:

  • 使用Default函数初始化Gin路由器。
  • 使用GET函数将GET HTTP方法和/albums路径与处理函数相关联。

请注意,你传递的是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状态码。

如上所述,真实世界的服务可能会使用数据库查询来执行此查找。

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服务。

建议的下一个主题:

全部代码

本节包含使用本教程构建的应用程序的全部代码:

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"})
}

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注