使用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星星数量排序:
它们提供了广泛的功能,例如路由,参数绑定,验证,中间件,其中一些甚至具有内置的ORM。
如果您更喜欢仅具有路由功能的轻量级软件包,那么以下是用于golang的一些最受欢迎的HTTP路由器:
在本教程中,我将使用最受欢迎的框架:Gin
安装Gin
让我们打开浏览器并搜索golang gin,然后打开其Github页面。向下滚动并选择Installation。
复制此go get命令,并在终端中运行它以安装软件包:
❯ go get -u github.com/gin-gonic/gin
此后,在go.mod我们的简单银行项目的文件中,我们可以看到gin和它使用的其他一些软件包一起作为新的依赖项添加了。
定义服务器结构
现在,我要创建一个名为的新文件夹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的这个功能:
在这里,我们可以看到将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请求。