使用Gin在Go中实现RESTful HTTP API

使用Gin在Go中实现RESTful HTTP API

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

技术开发 编程 技术框架 技术发展

 

使用Gin在Go中实现RESTful HTTP API

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

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

Gin 特性

  • 快速:路由不使用反射,基于Radix树,内存占用少。

  • 中间件:HTTP请求,可先经过一系列中间件处理,例如:Logger,Authorization,GZIP等。这个特性和 NodeJs 的 Koa 框架很像。中间件机制也极大地提高了框架的可扩展性。

  • 异常处理:服务始终可用,不会宕机。Gin 可以捕获 panic,并恢复。而且有极为便利的机制处理HTTP请求过程中发生的错误。

  • JSON:Gin可以解析并验证请求的JSON。这个特性对Restful API的开发尤其有用。

  • 路由分组:例如将需要授权和不需要授权的API分组,不同版本的API分组。而且分组可嵌套,且性能不受影响。

  • 渲染内置:原生支持JSON,XML和HTML的渲染。

以下是一些最受欢迎的golang网络框架,按其Github星星数量排序:

image.png

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

如果您更喜欢仅具有路由功能的轻量级软件包,那么以下是用于golang的一些最受欢迎的HTTP路由器:

image.png

在本教程中,我将使用最受欢迎的框架:Gin

安装Gin

让我们打开浏览器并搜索golang gin,然后打开其Github页面。向下滚动并选择Installation。

复制此go get命令,并在终端中运行它以安装软件包:

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

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

image.png

定义服务器结构

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

首先让我们定义一个新的Server结构。该服务器将为我们的银行服务提供所有HTTP请求。它将具有2个字段:

第一个是db.Store我们在之前的讲座中已经实现的。在处理来自客户端的API请求时,它将允许我们与数据库进行交互。

第二个字段是类型的路由器gin.Engine。该路由器将帮助我们将每个API请求发送到正确的处理程序进行处理。

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

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

首先,我们Server使用input创建一个新对象store。然后,我们通过调用来创建新的路由器gin.Default()。稍后我们将为此添加路线router。完成此步骤后,我们将router对象分配给server.router服务器并返回服务器。

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}

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

实施创建帐户API

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

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

为什么具有此功能签名?让我们看一下router.POSTGin的这个功能:

image.png

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

好了,现在让我们声明一个新的结构来存储创建帐户请求。这将有几个领域,类似createAccountParams的account.sql.go,我们在数据库中使用一讲:

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

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

type createAccountRequest struct {
    Owner    string `json:"owner"`
    Currency string `json:"currency"`}func (server *Server) createAccount(ctx *gin.Context) {
    ...}

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

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

我要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类型为type的新变量createAccountRequest。然后我们调用ctx.ShouldBindJSON()函数,并传入该req对象。此函数将返回错误。

如果错误不是nil,则表示客户端提供了无效数据。因此,我们应该向客户端发送一个400错误的请求响应。为此,我们只需要调用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
    }
    ...}

稍后我们将errorResponse()在代码中大量使用此功能,并且该功能还可以用于其他处理程序,而不仅用于帐户处理程序,因此我将在server.go文件中实现该功能。

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

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

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

现在让我们回到createAccount处理程序。如果输入数据有效,则不会有错误。因此,我们只是继续向数据库中插入一个新帐户。

首先,我们声明一个CreateAccountParams对象,其中Owneris req.Owner,Currencyisreq.Currency和Balanceis 0。然后我们调用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)}

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

如果没有错误发生,则说明帐户已成功创建。我们只发送一个200 OK状态代码,并将创建的帐户对象发送给客户端。就是这样!该createAccount处理器完成。

启动HTTP服务器

接下来,我们必须添加更多代码以运行HTTP服务器。我Start()要向我们的Server结构添加一个新函数。此函数将以addressas作为输入并返回错误。它的作用是在输入上运行HTTP服务器address以开始侦听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函数中。

通过此连接,我们可以创建一个新的storeusingdb.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)
    ...}

要启动服务器,我们只需要调用server.Start()并传递服务器地址即可。现在,我只是将其声明为常量:localhost,端口8080。将来,我们将重构代码以从环境变量或设置文件加载所有这些配置。万一启动服务器时发生错误,我们只写一条致命日志,说不能启动服务器。

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

package mainimport (
    "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)
    }}

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

我要叫它make server。并且它应该执行此go run main.go命令。让我们将服务器添加到伪列表中。

...server:
    go run main.go.PHONY: postgres createdb dropdb migrateup migratedown sqlc test server

然后打开终端并运行:

make server

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

image.png

技术开发 编程 技术框架 技术发展