所有文章 > API开发 > 使用Gin在Go中实现RESTful HTTP API
使用Gin在Go中实现RESTful HTTP API

使用Gin在Go中实现RESTful HTTP API

大家好,欢迎再次回到后端大师班。到目前为止,我们已经深入了解了在 Go 语言中操作数据库的知识。现在,是时候学习如何构建一些 RESTful HTTP API 了,这些 API 将为前端客户端与我们的银行服务后端之间的交互提供支持。

一、Go Web 框架和 HTTP 路由器

虽然我们可以直接使用标准的 net/http 包来构建这些 API,但借助一些现有的 Web 框架会让开发过程更加轻松。

下面是一些最受欢迎的 Go Web 框架,按其在 Github 上的受欢迎程度(星标数量)排序:

替换文本
  • Gin
  • Beego
  • Echo
  • Revel
  • Martini
  • Fiber
  • Buffalo

它们提供了丰富的功能,例如路由、参数绑定、数据验证以及中间件等,其中一些框架甚至内置了 ORM(对象关系映射)功能。

如果您更倾向于选择仅具备路由功能的轻量级软件包,那么以下是一些备受欢迎的 golang HTTP 路由器选项:

Alt Text
  • Fast HTTP
  • Gorilla Mux
  • HTTP Router
  • Chi

在本教程中,我将使用最流行的框架:Gin

二、Gin框架实现操作指南

1、安装 Gin

首先,请打开您的浏览器,搜索“golang gin”,接着打开 Gin 的 Github 页面。在页面上向下滚动一点,找到“Installation”部分。

接下来,复制提供的 go get 命令,并在您的终端中执行该命令以安装 Gin 软件包。

❯ go get -u github.com/gin-gonic/gin

在此之后,在我们的简单银行项目的go.mod文件中,我们可以看到gin作为一个新的依赖项与它使用的其他一些包一起添加。

Alt Text

2、定义服务器结构

现在,我将创建一个名为 api 的新文件夹,并在其中新建一个文件 server.go。这个文件将作为我们实现 HTTP API 服务器的主要场所。

首先,我们需要定义一个新的 Server 结构。这个服务器将负责处理所有针对我们银行服务的 HTTP 请求。它将包含两个关键字段:

  • 第一个字段是我们在之前的课程中已经实现的 db.Store。它将允许我们在处理来自客户端的 API 请求时与数据库进行交互。
  • 第二个字段是一个类型为 gin.Engine 的路由器。这个路由器将协助我们将每个 API 请求路由到正确的处理程序进行处理。
type Server struct {
store *db.Store
router *gin.Engine
}

现在,让我们添加一个名为 NewServer 的函数。这个函数将接收一个 db.Store 作为输入参数,并返回一个 Server 实例。此函数的职责是创建一个新的 Server 实例,并在这个服务器上为我们的服务配置所有的 HTTP API 路由。

首先,我们会使用传入的 db.Store 来初始化 Server 实例中的 store 字段。接着,通过调用 gin.Default() 方法来创建一个新的路由器实例。虽然我们现在还不会为 router 添加具体的路由,但在这一步之后,我们会将 router 对象赋值给 server.router,并最终返回这个 Server 实例。

func NewServer(store *db.Store) *Server {
server := &Server{store: store}
router := gin.Default()

// TODO: add routes to router

server.router = router
return server
}

现在,让我们添加第一个 API 路由,用于创建一个新账户。由于这个操作需要使用 POST 方法,因此我们会调用 router.POST 方法进行设置。

我们必须传入路由的路径,在本例中为/accounts,然后传入一个或多个处理函数。如果传入多个函数,那么最后一个应该是真实的处理程序,其他所有函数都应该是中间件。

func NewServer(store *db.Store) *Server {
server := &Server{store: store}
router := gin.Default()

router.POST("/accounts", server.createAccount)

server.router = router
return server
}

目前,我们还没有配置任何中间件,所以只需传入一个处理程序函数:server.createAccount。这是 Server 结构体中需要我们实现的一个方法。该方法需要作为 Server 结构体的方法,因为为了将新账户保存到数据库中,我们必须访问 store 对象。

3、实施创建账户 API

接下来,我将在 server 文件夹内新建一个文件 account.go,并在其中实现这个 API 方法。我们会声明一个带有 Server 指针接收器的函数,函数名为 createAccount。这个函数将接收一个 gin.Context 对象作为参数。

func (server *Server) createAccount(ctx *gin.Context) {
...
}

为什么它有这个函数签名?让我们看看Gin的这个router.POST函数:

Alt Text

在这里,我们可以看到 HandlerFunc 被定义为一个接收 Context 作为输入的函数。基本上,在使用 Gin 框架时,我们的处理程序中所进行的所有操作都会涉及到这个 Context 对象。它提供了许多便捷的方法,用于读取输入参数和发送响应。

接下来,让我们声明一个新的结构体,用于存储创建账户请求的数据。这个结构体将包含几个字段,这些字段与我们在上一节课中在数据库中使用的 createAccountParams 结构体以及在 account.sql.go 文件中定义的字段类似。

type CreateAccountParams struct {
Owner string `json:"owner"`
Balance int64 `json:"balance"`
Currency string `json:"currency"`
}

因此,我会复制这些字段并将它们粘贴到我们的 createAccountRequest 结构体中。考虑到在创建一个新账户时,其初始余额应该始终设置为0,所以我们可以省略 balance 字段。我们仅允许用户指定账户的所有者姓名和货币类型。这些输入参数将从 HTTP 请求的主体中获取,该主体是一个 JSON 对象,因此我会保留这些字段的 JSON 标签。

type createAccountRequest struct {
Owner string `json:"owner"`
Currency string `json:"currency"`
}

func (server *Server) createAccount(ctx *gin.Context) {
...
}

现在,每当从客户端接收输入数据时,进行验证总是一个明智的选择,因为客户端可能会发送一些无效或我们不希望存储在数据库中的数据。

幸运的是,Gin 框架内部使用了一个验证包来自动执行数据验证。例如,我们可以利用 binding 标签来告知 Gin 某个字段是必需的。随后,通过调用 ShouldBindJSON 函数来解析 HTTP 请求体中的输入数据,Gin 会自动验证输出对象,以确保其满足我们在 binding 标签中指定的条件。

我将为 owner 和 currency 字段添加 binding:"required" 标签。此外,假设我们的银行当前仅支持两种货币:USD 和 EUR。为了实现这一限制条件,我们可以使用 oneof 验证规则。

type createAccountRequest struct {
Owner string `json:"owner" binding:"required"`
Currency string `json:"currency" binding:"required,oneof=USD EUR"`
}

我们使用逗号来分隔多个验证条件,而对于 oneof 条件,则使用空格来分隔其可能的值。

现在,在 createAccount 函数中,我们首先声明了一个名为 req 的变量,其类型为 createAccountRequest。接着,我们调用 ctx.ShouldBindJSON() 函数,并将 req 对象作为参数传入。此函数会返回一个错误对象。

如果返回的错误对象不是 nil,则表示客户端提供了无效的数据。在这种情况下,我们应该向客户端发送一个 400 Bad Request 响应。为此,我们只需调用 ctx.JSON() 函数来发送 JSON 格式的响应。

ctx.JSON() 函数的第一个参数是 HTTP 状态码,在本例中应为 http.StatusBadRequest。第二个参数是我们希望发送给客户端的 JSON 对象。在这里,我们只需要发送错误信息,因此我们需要一个函数来将错误转换为键值对形式的对象,以便 Gin 可以在将其返回给客户端之前将其序列化为 JSON 格式。

func (server *Server) createAccount(ctx *gin.Context) {
var req createAccountRequest
if err := ctx.ShouldBindJSON(&req); err != nil {
ctx.JSON(http.StatusBadRequest, errorResponse(err))
return
}

...
}

我们将在后续代码中频繁使用 errorResponse 函数,它不仅限于账户处理程序,也可以应用于其他处理程序。因此,我计划在 server.go 文件中实现这个函数。

errorResponse 函数将接收一个错误作为输入参数,并返回一个 gin.H 对象。gin.H 实际上是 map[string]interface{} 的一个便捷类型,允许我们在其中存储任意类型的键值数据。

现在,我们先实现一个基本版本,该函数仅返回一个包含单个键 error 的 gin.H 对象,其值为错误消息。未来,我们可能会根据错误类型进行更细致的处理,并将其转换为更合适的格式。

func errorResponse(err error) gin.H {
return gin.H{"error": err.Error()}
}

现在,让我们再次关注 createAccount 处理程序。如果输入数据有效,那么 ShouldBindJSON 函数将不会返回错误。接下来,我们需要在数据库中插入一个新的账户。

首先,我们声明一个 AccountParams 对象,并设置其 Owner 字段为 req.OwnerCurrency 字段为 req.Currency,而 Balance 字段则设置为 0。然后,我们调用 server.store.CreateAccount 方法,传入当前的上下文(ctx)和刚才创建的 AccountParams 对象。这个方法会返回新创建的账户实例以及一个可能发生的错误。

func (server *Server) createAccount(ctx *gin.Context) {
var req createAccountRequest
if err := ctx.ShouldBindJSON(&req); err != nil {
ctx.JSON(http.StatusBadRequest, errorResponse(err))
return
}

arg := db.CreateAccountParams{
Owner: req.Owner,
Currency: req.Currency,
Balance: 0,
}

account, err := server.store.CreateAccount(ctx, arg)
if err != nil {
ctx.JSON(http.StatusInternalServerError, errorResponse(err))
return
}

ctx.JSON(http.StatusOK, account)
}

如果 CreateAccount 方法返回的错误不是 nil,这意味着在尝试将数据插入数据库时遇到了内部问题。因此,我们将向客户端返回一个 500 Internal Server Error 状态码,并重用 errorResponse 函数来将错误信息发送给客户端,然后立即结束处理。

如果 CreateAccount 方法没有返回错误,那么账户就成功创建了。此时,我们只需向客户端发送一个 200 OK 状态码,以及新创建的账户对象。至此,createAccount 处理程序就完成了。

4、启动 HTTP 服务器

接下来,我们需要添加更多代码来启动 HTTP 服务器。为此,我们将在 Server 结构体中添加一个新的方法 Start。这个方法将接收一个 address 字符串作为输入参数,并返回一个错误对象。Start 方法的作用是在指定的 address 上启动 HTTP 服务器,以便开始监听并处理 API 请求。

func (server *Server) Start(address string) error {
return server.router.Run(address)
}

Gin 框架已经在路由器中内置了启动服务器的功能,所以我们只需要调用 server.router.Run() 方法,并传入服务器地址即可。

值得注意的是,server.router 字段是私有的,这意味着它不能从 api 包的外部被直接访问。这正是我们提供公共 Start() 函数的原因之一。目前,这个函数只包含启动服务器的命令,但未来我们可能会在其中添加优雅的关闭逻辑等功能。

现在,让我们在存储库的根目录下创建一个 main.go 文件,作为我们服务器的入口点。该文件的包名应为 main,并且包含一个 main() 函数。

为了创建 Server 实例,我们需要先连接到数据库,并创建一个 Store。这个过程与我们之前在 main_test.go 文件中编写的代码非常相似。

因此,我会将用于数据库连接的常量 dbDriver 和 dbSource 复制到 main.go 文件的顶部。接着,将建立数据库连接的代码块也复制到 main() 函数中。

使用这个数据库连接,我们可以通过 db.NewStore() 函数创建一个新的 Store 实例。然后,通过调用 api.NewServer() 并传入这个 Store 实例,来创建一个新的服务器。

const (
dbDriver = "postgres"
dbSource = "postgresql://root:secret@localhost:5432/simple_bank?sslmode=disable"
)

func main() {
conn, err := sql.Open(dbDriver, dbSource)
if err != nil {
log.Fatal("cannot connect to db:", err)
}

store := db.NewStore(conn)
server := api.NewServer(store)

...
}

为了启动服务器,我们只需调用 server.Start() 方法,并传入服务器地址。现在,我将服务器地址声明为一个常量,即 localhost 的 8080 端口。未来,我们计划重构代码,以便从环境变量或配置文件中加载这些配置。如果服务器启动过程中遇到错误,我们将记录一条致命日志,指出服务器无法启动。

最后,还有一件非常关键的事情需要注意:我们需要在代码中为 lib/pq(PostgreSQL 数据库驱动程序)添加一个空白导入。没有这个导入,我们的代码将无法与数据库进行通信。

package main

import (
"database/sql"
"log"

_ "github.com/lib/pq"
"github.com/techschool/simplebank/api"
db "github.com/techschool/simplebank/db/sqlc"
)

const (
dbDriver = "postgres"
dbSource = "postgresql://root:secret@localhost:5432/simple_bank?sslmode=disable"
serverAddress = "0.0.0.0:8080"
)

func main() {
conn, err := sql.Open(dbDriver, dbSource)
if err != nil {
log.Fatal("cannot connect to db:", err)
}

store := db.NewStore(conn)
server := api.NewServer(store)

err = server.Start(serverAddress)
if err != nil {
log.Fatal("cannot start server:", err)
}
}

现在,我们的服务器主条目已经设置完毕。接下来,我们将在 Makefile 中添加一个新的命令来运行它。

我将这个命令命名为 server。执行 make server 时,它将启动我们的服务器。同时,我们也将把这个命令添加到伪目标(phony target)列表中。

...

server:
go run main.go

.PHONY: postgres createdb dropdb migrateup migratedown sqlc test server

然后打开终端并运行:

make server
Alt Text

服务器已经启动并在端口 8080 上监听,准备处理 HTTP 请求。

5、使用 Postman使用教程 测试创建帐户 API

现在,我将利用Postman使用教程 来测试创建账户的 API。

首先,我会添加一个新的请求,选择 POST 方法,并输入请求的 URL,即 http://localhost:8080/accounts

由于参数需要通过 JSON 主体发送,因此我选择 Body 选项卡,进一步选择 Raw,并指定 JSON 格式。接下来,我需要添加两个输入字段:一个是所有者的名字(这里我将使用我的名字作为示例),另一个是货币类型(这里我们选择美元)。

{
"owner": "Quang Pham",
"currency": "USD"
}

确定,然后单击 发送.

Alt Text

测试成功了。我们收到了一个 200 OK 状态码,以及新创建的账户对象。该账户具有 ID(例如 ID=1)、余额为 0,所有者名字和货币类型也均正确无误。

接下来,让我们尝试发送一些无效数据,观察服务器的响应。我将两个字段(所有者和货币)都设置为空字符串,然后点击发送请求。

{
"owner": "",
"currency": ""
}
Alt Text

这一次,服务器返回了 400 Bad Request 状态码,并提示字段为必需的错误信息。不过,这个错误消息由于同时包含了两个字段的验证错误,显得有些难以阅读。这确实是我们在未来需要改进的地方。

接下来,我打算尝试输入一个无效的货币代码,比如 “xyz”,看看服务器会如何响应。

{
"owner": "Quang Pham",
"currency": "xyz"
}
Alt Text

这一次,服务器同样返回了 400 Bad Request 状态码,但错误消息有所不同。它指出验证在 oneof 标签上失败,这符合我们的预期,因为我们在代码中仅允许两个货币值:USD 和 EUR。

Gin 框架确实非常出色,它仅用几行代码就为我们完成了所有的输入绑定和验证工作。此外,Gin 还以一种易于人类阅读的格式打印了请求日志。

Alt Text

6、实施获取帐户 API

好的,接下来我们将添加一个API来通过ID获取特定账户。这个API与创建账户的API非常相似,因此我将复制该路由语句。

func NewServer(store *db.Store) *Server {
...

router.POST("/accounts", server.createAccount)
router.GET("/accounts/:id", server.getAccount)

...
}

在这里,我们将采用POST方法,而不是GET方法。这个路径应该包含我们要获取的账户ID,格式为/accounts/:id。请注意,在id前面有一个冒号,这是我们告诉Gin框架id是一个URI参数的方式。

接下来,我们需要在getAccount结构上实现一个新的服务器处理程序。让我们转到account.go文件来实现这个功能。与之前类似,我们声明了一个名为getAccountRequest的新结构体来存储输入参数。它将包含一个类型为int64ID字段。

由于ID是一个URI参数,我们不能再像以前那样从请求体中获取它。相反,我们使用uri标签来告诉Gin框架URI参数的名称。

type getAccountRequest struct {
ID int64 `uri:"id" binding:"required,min=1"`
}

我们为账户 ID 添加了一个绑定条件,即它是一个必填字段,且值必须有效。具体来说,我们不希望客户端发送无效的 ID,比如负数。为了向 Gin 框架传达这一要求,我们可以使用 min 条件,并将其设置为 1,因为 1 是账户 ID 的最小可能值。

接下来,在 server.getAccount 处理程序中,我们将执行与之前类似的操作。首先,我们声明一个新的变量 req,其类型为 getAccountRequest。但这次,我们不会调用 ShouldBindJSON 方法,而是应该调用 ShouldBindUri 方法来从 URI 中提取参数。

如果 ShouldBindUri 方法返回错误,我们将直接返回一个 400 Bad Request 状态码。如果没有错误发生,我们将调用 server.store.GetAccount() 方法,并传入 req.ID 来获取对应的账户。这个方法会返回一个 account 对象和一个可能发生的错误。

func (server *Server) getAccount(ctx *gin.Context) {
var req getAccountRequest
if err := ctx.ShouldBindUri(&req); err != nil {
ctx.JSON(http.StatusBadRequest, errorResponse(err))
return
}

account, err := server.store.GetAccount(ctx, req.ID)
if err != nil {
if err == sql.ErrNoRows {
ctx.JSON(http.StatusNotFound, errorResponse(err))
return
}

ctx.JSON(http.StatusInternalServerError, errorResponse(err))
return
}

ctx.JSON(http.StatusOK, account)
}

如果 server.store.GetAccount() 方法返回的错误不为 nil,则可能存在两种情况:

  • 第一种情况是从数据库查询数据时发生内部错误。在这种情况下,我们会向客户端返回 500 Internal Server Error 状态码。
  • 第二种情况是数据库中不存在具有该特定 ID 的账户。此时,我们接收到的错误应该是 sql.ErrNoRows。因此,我们需要检查错误是否为 sql.ErrNoRows,如果是,则向客户端返回 404 Not Found 状态码。

如果一切顺利,没有出现任何错误,我们将向客户端返回 200 OK 状态码以及对应的账户信息。至此,我们的 getAccount API 就开发完成了。

7、使用 Postman使用教程 测试获取帐户 API

接下来,让我们重新启动服务器,并打开 Postman使用教程 进行测试。

我们将使用 GET 方法添加一个新请求,请求的 URL 为 http://localhost:8080/accounts/1。在 URL 的末尾添加 /1 是因为我们希望获取 ID 为 1 的账户信息。现在,点击“发送”按钮进行测试。

Alt Text

请求成功,我们收到了一个200 OK状态代码,并找到了相应的账户。这个账户正是我们之前创建的。

现在,让我们尝试获取一个不存在的账户。我将ID更改为100:http://localhost:8080/accounts/100,然后再次点击“Send”(发送)。

Alt Text

这一次,我们收到了一个404 Not Found状态代码,以及一个错误提示:“sql no rows in result set”。这完全符合我们的预期。

让我们再试一次,这次使用一个负数ID:http://localhost:8080/accounts/-1。

Alt Text

现在我们遇到了一个问题,收到了一个 400 Bad Request 状态码,错误消息指出验证失败。不过,这并不影响我们确认 getAccount API 的整体运行状况是良好的。

8、实施列表账户 API

接下来,我将为您介绍如何通过分页功能来实现列表账户 API。

考虑到数据库中的账户数量可能会随着时间的推移而大幅增长,我们不应该在单个 API 调用中尝试查询并返回所有账户。因此,我们采用了分页策略,将记录分成多个小页面,这样客户端每次 API 请求就只需检索一个页面的数据。

这个 API 与其他 API 有所不同,因为我们不会从请求正文或 URI 路径中提取输入参数,而是会从查询字符串中获取它们。下面是一个请求示例:

Alt Text

我们有两个查询参数:page_id 和 page_sizepage_id 代表我们想要获取的页面索引,从第1页开始计数;而 page_size 则表示每一页中可以返回的最大记录数量。

这两个参数会被添加到请求 URL 的问号之后,如下所示:http://localhost:8080/accounts?page_id=1&page_size=5。正因为它们出现在 URL 的这一部分,所以被称为查询参数,以区别于像 getAccount 请求中的账户 ID 那样的 URI 参数。

现在,让我们回到代码中。我们将使用相同的 GET 方法来添加一个新的路由,但这次路径应仅设置为 /accounts,因为我们计划从查询中获取所需的参数。处理程序的名称应命名为 listAccounts

func NewServer(store *db.Store) *Server {
server := &Server{store: store}
router := gin.Default()

router.POST("/accounts", server.createAccount)
router.GET("/accounts/:id", server.getAccount)
router.GET("/accounts", server.listAccount)

server.router = router
return server
}

好的,让我们打开account.go文件来实现server.listAccount函数。这个函数与server.getAccount处理程序非常相似,所以我将复制它。然后,我将结构体的名称更改为listAccountRequest

这个结构体应该存储两个参数:PageIDPageSize。请注意,我们现在不是从URI中获取这些参数,而是从查询字符串中获取,因此我们不能使用uri标签。我们应该使用form标签。

type listAccountRequest struct {
PageID int32 `form:"page_id" binding:"required,min=1"`
PageSize int32 `form:"page_size" binding:"required,min=5,max=10"`
}

这两个参数都是必需的,最小的PageID应该是1。对于PageSize,我们不希望它太大或太小,所以我将其最小值设置为5条记录,最大值设置为10条记录。

现在,server.listAccount处理函数应该这样实现:

func (server *Server) listAccount(ctx *gin.Context) {
var req listAccountRequest
if err := ctx.ShouldBindQuery(&req); err != nil {
ctx.JSON(http.StatusBadRequest, errorResponse(err))
return
}

arg := db.ListAccountsParams{
Limit: req.PageSize,
Offset: (req.PageID - 1) * req.PageSize,
}

accounts, err := server.store.ListAccounts(ctx, arg)
if err != nil {
ctx.JSON(http.StatusInternalServerError, errorResponse(err))
return
}

ctx.JSON(http.StatusOK, accounts)
}

req 变量的类型应该被定义为 listAccountRequest。接下来,我们会使用另一个绑定函数 ShouldBindQuery 来告诉 Gin 框架从查询字符串中提取数据。

如果在数据绑定过程中发生错误,我们将直接返回 400 Bad Request 状态码。如果没有错误发生,我们会调用 server.store.ListAccounts() 方法从数据库中查询一页的账户记录。这个方法需要一个 ListAccountsParams 结构体作为输入参数,我们需要为其中的 Limit 和 Offset 字段提供值。

Limit 字段的值就是 req.PageSize。而 Offset 字段表示数据库应该跳过的记录数,这个值需要通过以下公式计算得出:(req.PageID - 1) * req.PageSize

ListAccounts 函数会返回一个账户列表和一个可能发生的错误。如果函数返回错误,我们将向客户端返回 500 Internal Server Error 状态码。如果一切顺利,我们将向客户端发送一个 200 OK 状态码以及查询到的账户列表。

至此,ListAccounts API 的开发就完成了。

9、使用 Postman使用教程 测试列表账户 API

接下来,让我们重新启动服务器,并打开 Postman使用教程来测试这个新的 API 请求。

Alt Text

测试很成功,但目前名单上只有一个账户,这是因为我们的数据库还比较空。事实上,到目前为止,我们只创建了一个账户。为了获取更多的随机数据,我们可以运行之前讲座中编写的数据库测试。

❯ make test

好了,现在我们的数据库中应该有很多账户。让我们重新发送此 API 请求。

Alt Text

现在返回的账户列表正好包含了5个账户。注意,ID为5的账户并未出现在列表中,我猜测它可能在测试过程中被删除了。而ID为6的账户则出现在了我们的结果中。

接下来,让我们尝试获取第二页的数据。

Alt Text

太好了,现在我们成功地获取了接下来的5个账户,它们的ID从7到11。这表明我们的分页功能运行得非常顺畅。

接下来,我想再尝试一次,不过这次是为了测试一个不存在的页面,比如第100页。

Alt Text

现在的情况是,当我们请求一个不存在的页面时,服务器会返回一个 null 响应体。虽然从技术上讲这没错,但我更倾向于服务器在这种情况下返回一个空列表,这样客户端处理起来会更方便。

返回空列表而不是 null

在sqlc为我们生成的account.sql.go文件中:

func (q *Queries) ListAccounts(ctx context.Context, arg ListAccountsParams) ([]Account, error) {
rows, err := q.db.QueryContext(ctx, listAccounts, arg.Limit, arg.Offset)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Account
for rows.Next() {
var i Account
if err := rows.Scan(
&i.ID,
&i.Owner,
&i.Balance,
&i.Currency,
&i.CreatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}

我们注意到 items 变量(类型为 []Account)在未被初始化的情况下被声明了。因此,当没有记录被添加时,它的值会默认为 nil 而不是一个空切片。

不过,在 sqlc 的最新版本(1.5.0版)中,引入了一个名为 emit_empty_slices 的新设置。这个设置可以指示 sqlc 在没有查询到结果时返回一个空切片,而不是 nil

emit_empty_slices 设置的默认值是 false,意味着默认情况下,sqlc 仍然会返回 nil。但是,如果我们将这个值设置为 true,那么由 many 查询返回的结果就会是一个空切片,而不是 nil

现在,让我们将这个新设置添加到我们的 sqlc.yaml 文件中,以便利用这一功能。

version: "1"
packages:
- name: "db"
path: "./db/sqlc"
queries: "./db/query/"
schema: "./db/migration/"
engine: "postgresql"
emit_json_tags: true
emit_prepared_queries: false
emit_interface: false
emit_exact_table_names: false
emit_empty_slices: true

保存它,然后打开终端将 sqlc 升级到最新版本。如果您使用的是 Mac 并使用 Homebrew,只需运行:

❯ brew upgrade sqlc

您可以通过运行以下命令来检查当前版本:

❯ sqlc version
v1.5.0

对我来说,它已经是最新版本:1.5.0,所以现在我要重新生成代码:

❯ make sqlc

回到Visual Studio代码。现在在account.sql.go文件中,我们可以看到items变量现在被初始化为一个空切片:

func (q *Queries) ListAccounts(ctx context.Context, arg ListAccountsParams) ([]Account, error) {
...

items := []Account{}

...
}

太棒了!我们重新启动了服务器,并在Postman使用教程上进行了测试。现在,当我发送请求时,如果请求的是不存在的页面或没有记录可返回,服务器会按预期返回一个空列表,而不是之前的null响应。

Alt Text

它确实奏效了!

现在,我想尝试一些无效的参数来看看系统的反应。比如,我将 page_size 设置为20,这个值超出了我们设定的最大约束(假设最大约束是10)。

Alt Text

这一次,我们收到了一个400 Bad Request状态代码,并且在page_size参数上显示了一个错误,提示说max的验证失败了。

让我们再试一次,使用page_size=1

Alt Text

现在我们仍然收到了一个400 Bad Request状态代码,但错误是因为page_id验证在required标记上失败了。在验证程序包中,任何零值都会被识别为缺失。在这种情况下,这是可以接受的,无论怎样,我们不希望有0页。

但是,如果您的API参数为零值,您需要注意这一点。我建议您阅读validator包的文档来了解更多。

好了,今天我们已经了解了在Go语言中使用Gin框架实现RESTful HTTP API是多么容易。您可以根据本教程尝试实现更多路由,来自行更新或删除账户。我把这个练习留给您。

非常感谢您阅读本文。祝您编码愉快!

原文链接:https://dev.to/techschoolguru/implement-restful-http-api-in-go-using-gin-4ap1

#你可能也喜欢这些API文章!