使用 Gin 在 Go 中实现 RESTful HTTP API

2025-06-07

使用 Gin 在 Go 中实现 RESTful HTTP API

您好,欢迎回到后端大师班。

到目前为止,我们已经学习了很多关于用 Go 语言操作数据库的知识。现在是时候学习如何实现一些 RESTful HTTP API,以便前端客户端能够与我们的银行服务后端进行交互。

以下是:

Go Web 框架和 HTTP 路由器

虽然我们可以使用标准的 net/http 包来实现这些 API,但利用一些现有的 Web 框架会容易得多。

以下是一些最受欢迎的 golang web 框架,按 Github 星号排序:

替代文本

它们提供了广泛的功能,例如路由、参数绑定、验证、中间件,其中一些甚至具有内置 ORM。

如果您更喜欢仅具有路由功能的轻量级包,那么这里有一些最流行的 golang HTTP 路由器:

替代文本

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

安装 Gin

我们打开浏览器搜索golang gin,然后打开它的Github 页面。向下滚动一点,然后选择Installation

让我们复制这个 go get 命令,并在终端中运行它来安装包:



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


Enter fullscreen mode Exit fullscreen mode

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

替代文本

定义服务器结构

现在我要创建一个名为 的新文件夹api。然后在里面创建一个新文件server.go。这就是我们实现 HTTP API 服务器的地方。

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

  • 第一个是db.Store我们在之前的课程中实现的。它将允许我们在处理来自客户端的 API 请求时与数据库进行交互。
  • 第二个字段是 类型的路由器gin.Engine。这个路由器会帮助我们将每个 API 请求发送到正确的处理程序进行处理。


type Server struct {
    store  *db.Store
    router *gin.Engine
}


Enter fullscreen mode Exit fullscreen mode

现在让我们添加一个函数NewServer,它接受 adb.Store作为输入,并返回 a Server。此函数将创建一个新Server实例,并为该服务器上的服务设置所有 HTTP API 路由。

Server首先,我们用输入创建一个新的对象store。然后通过调用创建一个新的路由器。稍后gin.Default()我们将向其添加路由。完成此步骤后,我们将对象赋值给服务器并返回服务器。routerrouterserver.router



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

    // TODO: add routes to router

    server.router = router
    return server
}


Enter fullscreen mode Exit fullscreen mode

现在让我们添加第一个 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
}


Enter fullscreen mode Exit fullscreen mode

目前我们没有任何中间件,所以我只传入一个 handler: 。这是我们需要实现的结构server.createAccount体方法。之所以需要将其作为结构体方法,是因为我们必须访问该对象才能将新账户保存到数据库。ServerServerstore

实现创建帐户 API

我将在文件夹内的server.createAccount一个新文件中实现方法。这里我们声明一个带有服务器指针接收器的函数。它的名称是,并且应该以一个对象作为输入。account.goapicreateAccountgin.Context



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


Enter fullscreen mode Exit fullscreen mode

为什么会有这个函数签名呢?我们来看看router.POSTGin的这个函数:

替代文本

这里我们可以看到,它HandlerFunc被声明为一个带有输入的函数Context。基本上,在使用 Gin 时,我们在处理程序中执行的所有操作都会涉及到这个上下文对象。它提供了许多便捷的方法来读取输入参数并输出响应。

好了,现在让我们声明一个新的结构体来存储创建账户的请求。它将包含几个字段,类似于我们在上一节课中数据库中使用的createAccountParams字段account.sql.go



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


Enter fullscreen mode Exit fullscreen mode

我要复制这些字段并粘贴到我们的createAccountRequest结构体中。新账户创建时,其初始余额应始终为 0,因此我们可以删除余额字段。我们只允许客户端指定账户所有者的姓名和货币。我们将从 HTTP 请求的主体中获取这些输入参数,该主体是一个 JSON 对象,因此我将保留 JSON 标签。



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

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


Enter fullscreen mode Exit fullscreen mode

现在,每当我们从客户端获取输入数据时,验证它们总是一个好主意,因为谁知道呢,客户端可能会发送一些我们不想存储在数据库中的无效数据。

幸运的是,Gin 内部使用了一个验证器包,可以在后台自动执行数据验证。例如,我们可以使用一个binding标签告诉 Gin 该字段是required。之后,我们调用该ShouldBindJSON函数从 HTTP 请求正文中解析输入数据,Gin 将验证输出对象,以确保它满足我们在绑定标签中指定的条件。

我将为requiredowner 和 currency 字段添加一个绑定标签。此外,假设我们的银行目前仅支持两种货币:USDEUR。那么我们如何让 Gin 帮我们检查呢?好吧,我们可以使用oneof条件来实现这一点:



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


Enter fullscreen mode Exit fullscreen mode

我们使用逗号分隔多个条件,使用空格分隔oneof条件的可能值。

好了,现在createAccount我们在函数中声明一个req类型的新变量createAccountRequest。然后我们调用ctx.ShouldBindJSON()函数,并传入这个req对象。这个函数将返回一个错误。

如果错误不是nil,则表示客户端提供了无效数据。因此,我们应该向客户端发送 400 Bad Request 响应。为此,我们只需调用ctx.JSON()函数发送 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
    }

    ...
}


Enter fullscreen mode Exit fullscreen mode

我们稍后会errorResponse()在代码中大量使用这个函数,它也可以用于其他处理程序,而不仅仅是帐户处理程序,所以我将在server.go文件中实现它。

此函数将错误作为输入,并返回一个gin.H对象,该对象实际上只是 的快捷方式map[string]interface{}。因此,我们可以在其中存储任何所需的键值数据。

现在我们只返回一个键为 error 的 gin.H,它的值是错误消息。稍后我们可能会检查错误类型,并根据需要将其转换为更合适的格式。



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


Enter fullscreen mode Exit fullscreen mode

现在让我们回到createAccount处理程序。如果输入数据有效,就不会出现错误。所以我们继续将新账户插入数据库。

首先,我们声明一个 CreateAccountParams 对象,其中Ownerreq.OwnerCurrencyreq.CurrencyBalance0。然后我们调用server.store.CreateAccount(),传入输入上下文和参数 。此函数将返回创建的帐户和一个错误。



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


Enter fullscreen mode Exit fullscreen mode

如果错误不是nil,那么在尝试插入数据库时​​一定存在一些内部问题。因此,我们将向500 Internal Server Error客户端返回状态码。我们还重用了该errorResponse()函数将错误发送给客户端,然后立即返回。

如果没有错误发生,则帐户创建成功。我们只需200 OK向客户端发送一个状态码和创建的帐户对象即可。就这样!createAccount处理程序完成了。

启动 HTTP 服务器

接下来,我们需要添加一些代码来运行 HTTP 服务器。我将Start()Server结构体中添加一个新函数。该函数将接受一个错误address作为输入并返回一个错误。它的作用是在输入上运行 HTTP 服务器,address以开始监听 API 请求。



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


Enter fullscreen mode Exit fullscreen mode

Gin 已经在路由器中提供了一个函数来执行此操作,因此我们需要做的就是调用server.router.Run(),并传入服务器地址。

请注意,该server.router字段是私有的,因此无法从包外部访问api。这也是我们创建这个公共函数的原因之一Start()。目前,它只有一个命令,但以后我们可能还想在这个函数中添加一些优雅的关闭逻辑。

好的,现在让我们在main.go这个仓库根目录下的文件中为我们的服务器创建一个入口点。包名称应该是main,并且应该有一个main()函数。

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

我要复制这些常量,dbDriverdbSource粘贴到文件顶部main.go。然后复制建立数据库连接的代码块,并将其粘贴到主函数中。

有了这个连接,我们可以创建一个新的storeusing函数。然后我们通过调用并传入 来db.NewStore()创建一个新的服务器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)

    ...
}


Enter fullscreen mode Exit fullscreen mode

要启动服务器,我们只需调用server.Start()并传入服务器地址即可。目前,我将其声明为常量:localhost,端口 8080。将来,我们将重构代码,以便从环境变量或设置文件加载所有这些配置。如果在启动服务器时出现错误,我们只需写入致命日志,提示“无法启动服务器”。

最后但非常重要的一点是,我们必须为驱动程序添加一个空白的导入lib/pq。如果没有这个,我们的代码将无法与数据库通信。



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


Enter fullscreen mode Exit fullscreen mode

好了,现在我们服务器的主入口已经完成了。让我们添加一个新的 make 命令来Makefile运行它。

我要调用它make server。它应该执行这个go run main.go命令。让我们将服务器添加到伪造列表中。



...

server:
    go run main.go

.PHONY: postgres createdb dropdb migrateup migratedown sqlc test server


Enter fullscreen mode Exit fullscreen mode

然后打开终端并运行:



make server


Enter fullscreen mode Exit fullscreen mode

替代文本

瞧,服务器已启动并运行。它正在监听并处理 8080 端口上的 HTTP 请求。

使用 Postman 测试创建帐户 API

现在我将使用 Postman 来测试创建帐户 API。

我们来添加一个新的请求,选择POST方法,填写URL,即http://localhost:8080/accounts

参数应该通过 JSON 格式发送,因此我们选择选项Body卡,选择Raw,然后选择JSON格式。我们需要添加两个输入字段:所有者姓名(此处我将使用我的名字)和货币,例如美元。



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


Enter fullscreen mode Exit fullscreen mode

确定,然后单击“发送”。

替代文本

耶,成功了。我们得到了200 OK状态码,以及创建的账户对象。它包含ID = 1balance = 0,以及正确的所有者姓名和货币。

现在让我们尝试发送一些无效数据来看看会发生什么。我将把两个字段都设置为空字符串,然后点击“发送”。



{
    "owner": "",
    "currency": ""
}


Enter fullscreen mode Exit fullscreen mode

替代文本

这次,我们得到了400 Bad Request,以及一个错误,提示字段为必填项。这个错误信息看起来很难阅读,因为它把两个字段的验证错误合并在一起了。这是我们将来可能想要改进的地方。

接下来我将尝试使用无效的货币代码,例如xyz



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


Enter fullscreen mode Exit fullscreen mode

替代文本

这次,我们也得到了400 Bad Request状态码,但错误信息有所不同。它表示标签验证失败oneof,这正是我们想要的,因为在代码中,我们只允许货币有 2 个可能的值:USDEUR

Gin 只用几行代码就帮我们处理了所有输入绑定和验证,真是太棒了。它还能打印出清晰易读的请求日志。

替代文本

实现获取账户 API

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



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

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

    ...
}


Enter fullscreen mode Exit fullscreen mode

这里POST我们将使用GET方法而不是 。此路径应包含id我们要获取的账户的/accounts/:id。请注意, 前面有一个冒号id。这就是我们告诉 Gin 它id是一个 URI 参数的方式。

然后,我们必须getAccount在该Server结构体上实现一个新的处理程序。让我们转到account.go文件来执行此操作。与之前类似,我们声明一个名为 的结构体getAccountRequest来存储输入参数。它将包含一个ID类型为 的字段int64

现在,由于ID是一个 URI 参数,我们无法像以前一样从请求正文中获取它。相反,我们使用uri标签来告诉 Gin URI 参数的名称:



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


Enter fullscreen mode Exit fullscreen mode

我们添加一个绑定条件,即这ID是必填字段。此外,我们不希望客户端发送无效的 ID,例如负数。为了告知 Gin 这一点,我们可以使用最小值条件。在本例中,我们设置min = 1,因为它是账户 ID 的最小可能值。

好的,现在在server.getAccount处理程序中,我们将执行与之前类似的操作。首先,我们声明一个req类型的新变量。然后在这里,我们应该调用 ,getAccountRequest而不是ShouldBindJSONShouldBindUri

如果发生错误,我们只返回一个400 Bad Request状态码。否则,我们调用来获取等于 的server.store.GetAccount()帐户。此函数将返回一个和一个错误。IDreq.IDaccount



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


Enter fullscreen mode Exit fullscreen mode

如果错误不是nil,则有两种可能的情况。

  • 第一种情况是从数据库查询数据时出现一些内部错误。在这种情况下,我们只需500 Internal Server Error向客户端返回状态码。
  • 第二种情况是,当输入的特定 ID 对应的账户不存在时。在这种情况下,我们收到的错误应该是sql.ErrNoRows。所以我们只需在这里检查一下,如果确实存在,就直接404 Not Found向客户端发送一个状态码,然后返回。

如果一切顺利,没有错误,我们只需200 OK向客户端返回状态码和账户信息即可。就这样!我们的 getAccount API 完成了。

使用 Postman 测试获取帐户 API

我们重启服务器,打开Postman进行测试。

让我们添加一个方法为 GET 的新请求,URL 为http://localhost:8080/accounts/1。我们/1在末尾添加了一个,因为我们想要获取带有 的账户ID = 1。现在点击发送:

替代文本

请求成功,我们收到了200 OK状态码以及找到的账户。这正是我们之前创建的账户。

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

替代文本

这次我们得到了404 Not Found状态代码和错误:sql no rows in result set。正如我们所料。

让我们再试一次使用负面 ID:http://localhost:8080/accounts/-1

替代文本

现在我们得到了一个400 Bad Request状态代码,其中包含有关验证失败的错误消息。

好的,我们的 getAccount API 运行良好。

实现列出帐户 API

下一步,我将向您展示如何实现带有分页的列表帐户 API。

随着时间的推移,我们数据库中存储的账户数量可能会增长到非常大的规模。因此,我们不应该在一次 API 调用中查询并返回所有账户。分页的理念是将记录分成多个小页面,以便客户端每次 API 请求只能检索一页。

此 API 略有不同,因为我们不会从请求正文或 URI 获取输入参数,而是从查询字符串中获取。以下是请求示例:

替代文本

我们有一个page_id参数,它是我们想要获取的页面的索引号,从第 1 页开始。还有一个page_size参数,它是 1 页中可以返回的最大记录数。

如您所见,page_idpage_size被添加到请求 URL 的问号后面:http://localhost:8080/accounts?page_id=1&page_size=5。这就是为什么它们被称为查询参数,而不是像获取帐户请求中的帐户 ID 那样的 URI 参数。

好的,现在让我们回到代码。我将使用相同的GET方法添加一条新路由。但这次,路径应该是/accountsonly,因为我们要从查询中获取参数。处理程序的名称应该是listAccount



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
}


Enter fullscreen mode Exit fullscreen mode

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

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



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


Enter fullscreen mode Exit fullscreen mode

两个参数都是必需的,最小值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)
}


Enter fullscreen mode Exit fullscreen mode

变量req的类型应该是listAccountRequest。然后我们使用另一个绑定函数:ShouldBindQuery告诉 Gin 从查询字符串中获取数据。

如果发生错误,我们只返回一个400 Bad Request状态。否则,我们调用 来server.store.ListAccounts()从数据库中查询一页帐户记录。此函数需要一个ListAccountsParams作为输入,我们需要为两个字段提供值:LimitOffset

Limit就是req.PageSize。虽然Offset是数据库应该跳过的记录数,但我们必须使用以下公式根据页面 ID 和页面大小来计算它:(req.PageID - 1) * req.PageSize

ListAccounts函数返回一个列表accounts和一个错误。如果发生错误,我们只需返回500 Internal Server Error客户端即可。否则,我们将发送一个200 OK状态码以及输出账户列表。

就这样,ListAccount API 就完成了。

使用 Postman 测试列表帐户 API

让我们重新启动服务器,然后打开 Postman 来测试这个请求。

替代文本

成功了,但列表中只有一个账户。这是因为我们的数据库目前很空。我们只创建了一个账户。让我们运行一下之前课程中编写的数据库测试,获取更多随机数据。



❯ make test


Enter fullscreen mode Exit fullscreen mode

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

替代文本

瞧,现在返回的列表恰好包含 5 个帐户,正如预期的那样。ID 为 5 的帐户没有出现在列表中,因为我认为它在测试中被删除了。我们在这里看到的是 ID 为 6 的帐户。

我们尝试获取第二页。

替代文本

太棒了,现在我们得到了接下来的 5 个帐户,ID 从 7 到 11。所以它运行得很好。

我将再尝试一次获取不存在的页面,比如说第 100 页。

替代文本

好的,现在我们得到了一个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
}


Enter fullscreen mode Exit fullscreen mode

我们可以看到,Accountitems 变量声明后没有被初始化:var items []Account。这就是为什么如果没有添加记录,它将保持为空。

幸运的是,在最新发布的 sqlc(版本 1.5.0)中,我们有一个新的设置,它将指示 sqlc 创建一个空切片而不是空值。

该设置称为emit_empty_slices,其默认值为false。如果我们将此值设置为true,则多查询返回的结果将为空切片。

好的,现在让我们将这个新设置添加到我们的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


Enter fullscreen mode Exit fullscreen mode

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



❯ brew upgrade sqlc


Enter fullscreen mode Exit fullscreen mode

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



❯ sqlc version
v1.5.0


Enter fullscreen mode Exit fullscreen mode

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



❯ make sqlc


Enter fullscreen mode Exit fullscreen mode

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



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

    items := []Account{}

    ...
}


Enter fullscreen mode Exit fullscreen mode

太棒了!让我们重启服务器并在 Postman 上测试一下。现在,当我发送这个请求时,我们得到了一个空列表,正如预期的那样。

替代文本

所以它有效!

现在我要尝试一些无效的参数。例如,让我们将其更改page_size20,它大于的最大约束10

替代文本

这次我们得到了400 Bad Request状态代码,以及一个错误,表明标签验证page_size失败max

让我们再试一次page_id = 0

替代文本

现在我们仍然能获取400 Bad Request状态,但错误是由于page_id标签验证失败required。这里发生的情况是,在验证器包中,任何零值都会被识别为缺失值。在这种情况下,这是可以接受的,因为我们无论如何都不想得到零值页面。

但是,如果你的 API 有一个零值参数,那么你需要注意这一点。我建议你阅读validator 包的文档来了解更多信息。

好了,今天我们已经学习了使用 Gin 在 Go 中轻松实现 RESTful HTTP API。您可以基于本教程尝试自行实现更多路由来更新或删除账户。我把这留作练习。

非常感谢你阅读这篇文章。祝你编程愉快!下节课再见!


如果您喜欢这篇文章,请订阅我们的 Youtube 频道在 Twitter 上关注我们,以便将来获取更多教程。


如果你想加入我目前在 Voodoo 的优秀团队,请查看我们的职位空缺。你可以远程办公,也可以在巴黎/阿姆斯特丹/伦敦/柏林/巴塞罗那现场办公,但需获得签证担保。

文章来源:https://dev.to/techschoolguru/implement-restful-http-api-in-go-using-gin-4ap1
PREV
gRPC 简介:为什么?是什么?如何?为什么要使用 gRPC?什么是 gRPC?它是如何工作的?gRPC 如何生成代码?
NEXT
如何安全地存储密码?