
如何快速實現(xiàn)REST API集成以優(yōu)化業(yè)務(wù)流程
它們提供了豐富的功能,例如路由、參數(shù)綁定、數(shù)據(jù)驗證以及中間件等,其中一些框架甚至內(nèi)置了 ORM(對象關(guān)系映射)功能。
如果您更傾向于選擇僅具備路由功能的輕量級軟件包,那么以下是一些備受歡迎的 golang HTTP 路由器選項:
在本教程中,我將使用最流行的框架:Gin
首先,請打開您的瀏覽器,搜索“golang gin”,接著打開 Gin 的 Github 頁面。在頁面上向下滾動一點,找到“Installation”部分。
接下來,復(fù)制提供的 go get
命令,并在您的終端中執(zhí)行該命令以安裝 Gin 軟件包。
? go get -u github.com/gin-gonic/gin
在此之后,在我們的簡單銀行項目的go.mod
文件中,我們可以看到gin作為一個新的依賴項與它使用的其他一些包一起添加。
現(xiàn)在,我將創(chuàng)建一個名為 api
的新文件夾,并在其中新建一個文件 server.go
。這個文件將作為我們實現(xiàn) HTTP API 服務(wù)器的主要場所。
首先,我們需要定義一個新的 Server
結(jié)構(gòu)。這個服務(wù)器將負(fù)責(zé)處理所有針對我們銀行服務(wù)的 HTTP 請求。它將包含兩個關(guān)鍵字段:
db.Store
。它將允許我們在處理來自客戶端的 API 請求時與數(shù)據(jù)庫進(jìn)行交互。gin.Engine
的路由器。這個路由器將協(xié)助我們將每個 API 請求路由到正確的處理程序進(jìn)行處理。type Server struct {
store *db.Store
router *gin.Engine
}
現(xiàn)在,讓我們添加一個名為 NewServer
的函數(shù)。這個函數(shù)將接收一個 db.Store
作為輸入?yún)?shù),并返回一個 Server
實例。此函數(shù)的職責(zé)是創(chuàng)建一個新的 Server
實例,并在這個服務(wù)器上為我們的服務(wù)配置所有的 HTTP API 路由。
首先,我們會使用傳入的 db.Store
來初始化 Server
實例中的 store
字段。接著,通過調(diào)用 gin.Default()
方法來創(chuàng)建一個新的路由器實例。雖然我們現(xiàn)在還不會為 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
}
現(xiàn)在,讓我們添加第一個 API 路由,用于創(chuàng)建一個新賬戶。由于這個操作需要使用 POST 方法,因此我們會調(diào)用 router.POST
方法進(jìn)行設(shè)置。
我們必須傳入路由的路徑,在本例中為/accounts
,然后傳入一個或多個處理函數(shù)。如果傳入多個函數(shù),那么最后一個應(yīng)該是真實的處理程序,其他所有函數(shù)都應(yīng)該是中間件。
func NewServer(store *db.Store) *Server {
server := &Server{store: store}
router := gin.Default()
router.POST("/accounts", server.createAccount)
server.router = router
return server
}
目前,我們還沒有配置任何中間件,所以只需傳入一個處理程序函數(shù):server.createAccount
。這是 Server
結(jié)構(gòu)體中需要我們實現(xiàn)的一個方法。該方法需要作為 Server
結(jié)構(gòu)體的方法,因為為了將新賬戶保存到數(shù)據(jù)庫中,我們必須訪問 store
對象。
接下來,我將在 server
文件夾內(nèi)新建一個文件 account.go
,并在其中實現(xiàn)這個 API 方法。我們會聲明一個帶有 Server
指針接收器的函數(shù),函數(shù)名為 createAccount
。這個函數(shù)將接收一個 gin.Context
對象作為參數(shù)。
func (server *Server) createAccount(ctx *gin.Context) {
...
}
為什么它有這個函數(shù)簽名?讓我們看看Gin的這個router.POST
函數(shù):
在這里,我們可以看到 HandlerFunc
被定義為一個接收 Context
作為輸入的函數(shù)。基本上,在使用 Gin 框架時,我們的處理程序中所進(jìn)行的所有操作都會涉及到這個 Context
對象。它提供了許多便捷的方法,用于讀取輸入?yún)?shù)和發(fā)送響應(yīng)。
接下來,讓我們聲明一個新的結(jié)構(gòu)體,用于存儲創(chuàng)建賬戶請求的數(shù)據(jù)。這個結(jié)構(gòu)體將包含幾個字段,這些字段與我們在上一節(jié)課中在數(shù)據(jù)庫中使用的 createAccountParams
結(jié)構(gòu)體以及在 account.sql.go
文件中定義的字段類似。
type CreateAccountParams struct {
Owner string json:"owner"
Balance int64 json:"balance"
Currency string json:"currency"
}
因此,我會復(fù)制這些字段并將它們粘貼到我們的 createAccountRequest
結(jié)構(gòu)體中。考慮到在創(chuàng)建一個新賬戶時,其初始余額應(yīng)該始終設(shè)置為0,所以我們可以省略 balance
字段。我們僅允許用戶指定賬戶的所有者姓名和貨幣類型。這些輸入?yún)?shù)將從 HTTP 請求的主體中獲取,該主體是一個 JSON 對象,因此我會保留這些字段的 JSON 標(biāo)簽。
type createAccountRequest struct {
Owner string json:"owner"
Currency string json:"currency"
}
func (server *Server) createAccount(ctx *gin.Context) {
...
}
現(xiàn)在,每當(dāng)從客戶端接收輸入數(shù)據(jù)時,進(jìn)行驗證總是一個明智的選擇,因為客戶端可能會發(fā)送一些無效或我們不希望存儲在數(shù)據(jù)庫中的數(shù)據(jù)。
幸運的是,Gin 框架內(nèi)部使用了一個驗證包來自動執(zhí)行數(shù)據(jù)驗證。例如,我們可以利用 binding
標(biāo)簽來告知 Gin 某個字段是必需的。隨后,通過調(diào)用 ShouldBindJSON
函數(shù)來解析 HTTP 請求體中的輸入數(shù)據(jù),Gin 會自動驗證輸出對象,以確保其滿足我們在 binding
標(biāo)簽中指定的條件。
我將為 owner
和 currency
字段添加 binding:"required"
標(biāo)簽。此外,假設(shè)我們的銀行當(dāng)前僅支持兩種貨幣:USD 和 EUR。為了實現(xiàn)這一限制條件,我們可以使用 oneof
驗證規(guī)則。
type createAccountRequest struct {
Owner string json:"owner" binding:"required"
Currency string json:"currency" binding:"required,oneof=USD EUR"
}
我們使用逗號來分隔多個驗證條件,而對于 oneof
條件,則使用空格來分隔其可能的值。
現(xiàn)在,在 createAccount
函數(shù)中,我們首先聲明了一個名為 req
的變量,其類型為 createAccountRequest
。接著,我們調(diào)用 ctx.ShouldBindJSON()
函數(shù),并將 req
對象作為參數(shù)傳入。此函數(shù)會返回一個錯誤對象。
如果返回的錯誤對象不是 nil
,則表示客戶端提供了無效的數(shù)據(jù)。在這種情況下,我們應(yīng)該向客戶端發(fā)送一個 400 Bad Request 響應(yīng)。為此,我們只需調(diào)用 ctx.JSON()
函數(shù)來發(fā)送 JSON 格式的響應(yīng)。
ctx.JSON()
函數(shù)的第一個參數(shù)是 HTTP 狀態(tài)碼,在本例中應(yīng)為 http.StatusBadRequest
。第二個參數(shù)是我們希望發(fā)送給客戶端的 JSON 對象。在這里,我們只需要發(fā)送錯誤信息,因此我們需要一個函數(shù)來將錯誤轉(zhuǎn)換為鍵值對形式的對象,以便 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
}
...
}
我們將在后續(xù)代碼中頻繁使用 errorResponse
函數(shù),它不僅限于賬戶處理程序,也可以應(yīng)用于其他處理程序。因此,我計劃在 server.go
文件中實現(xiàn)這個函數(shù)。
errorResponse
函數(shù)將接收一個錯誤作為輸入?yún)?shù),并返回一個 gin.H
對象。gin.H
實際上是 map[string]interface{}
的一個便捷類型,允許我們在其中存儲任意類型的鍵值數(shù)據(jù)。
現(xiàn)在,我們先實現(xiàn)一個基本版本,該函數(shù)僅返回一個包含單個鍵 error
的 gin.H
對象,其值為錯誤消息。未來,我們可能會根據(jù)錯誤類型進(jìn)行更細(xì)致的處理,并將其轉(zhuǎn)換為更合適的格式。
func errorResponse(err error) gin.H {
return gin.H{"error": err.Error()}
}
現(xiàn)在,讓我們再次關(guān)注 createAccount
處理程序。如果輸入數(shù)據(jù)有效,那么 ShouldBindJSON
函數(shù)將不會返回錯誤。接下來,我們需要在數(shù)據(jù)庫中插入一個新的賬戶。
首先,我們聲明一個 AccountParams
對象,并設(shè)置其 Owner
字段為 req.Owner
,Currency
字段為 req.Currency
,而 Balance
字段則設(shè)置為 0。然后,我們調(diào)用 server.store.CreateAccount
方法,傳入當(dāng)前的上下文(ctx
)和剛才創(chuàng)建的 AccountParams
對象。這個方法會返回新創(chuàng)建的賬戶實例以及一個可能發(fā)生的錯誤。
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
,這意味著在嘗試將數(shù)據(jù)插入數(shù)據(jù)庫時遇到了內(nèi)部問題。因此,我們將向客戶端返回一個 500 Internal Server Error 狀態(tài)碼,并重用 errorResponse
函數(shù)來將錯誤信息發(fā)送給客戶端,然后立即結(jié)束處理。
如果 CreateAccount
方法沒有返回錯誤,那么賬戶就成功創(chuàng)建了。此時,我們只需向客戶端發(fā)送一個 200 OK 狀態(tài)碼,以及新創(chuàng)建的賬戶對象。至此,createAccount
處理程序就完成了。
接下來,我們需要添加更多代碼來啟動 HTTP 服務(wù)器。為此,我們將在 Server
結(jié)構(gòu)體中添加一個新的方法 Start
。這個方法將接收一個 address
字符串作為輸入?yún)?shù),并返回一個錯誤對象。Start
方法的作用是在指定的 address
上啟動 HTTP 服務(wù)器,以便開始監(jiān)聽并處理 API 請求。
func (server *Server) Start(address string) error {
return server.router.Run(address)
}
Gin 框架已經(jīng)在路由器中內(nèi)置了啟動服務(wù)器的功能,所以我們只需要調(diào)用 server.router.Run()
方法,并傳入服務(wù)器地址即可。
值得注意的是,server.router
字段是私有的,這意味著它不能從 api
包的外部被直接訪問。這正是我們提供公共 Start()
函數(shù)的原因之一。目前,這個函數(shù)只包含啟動服務(wù)器的命令,但未來我們可能會在其中添加優(yōu)雅的關(guān)閉邏輯等功能。
現(xiàn)在,讓我們在存儲庫的根目錄下創(chuàng)建一個 main.go
文件,作為我們服務(wù)器的入口點。該文件的包名應(yīng)為 main
,并且包含一個 main()
函數(shù)。
為了創(chuàng)建 Server
實例,我們需要先連接到數(shù)據(jù)庫,并創(chuàng)建一個 Store
。這個過程與我們之前在 main_test.go
文件中編寫的代碼非常相似。
因此,我會將用于數(shù)據(jù)庫連接的常量 dbDriver
和 dbSource
復(fù)制到 main.go
文件的頂部。接著,將建立數(shù)據(jù)庫連接的代碼塊也復(fù)制到 main()
函數(shù)中。
使用這個數(shù)據(jù)庫連接,我們可以通過 db.NewStore()
函數(shù)創(chuàng)建一個新的 Store
實例。然后,通過調(diào)用 api.NewServer()
并傳入這個 Store
實例,來創(chuàng)建一個新的服務(wù)器。
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)
...
}
為了啟動服務(wù)器,我們只需調(diào)用 server.Start()
方法,并傳入服務(wù)器地址。現(xiàn)在,我將服務(wù)器地址聲明為一個常量,即 localhost
的 8080
端口。未來,我們計劃重構(gòu)代碼,以便從環(huán)境變量或配置文件中加載這些配置。如果服務(wù)器啟動過程中遇到錯誤,我們將記錄一條致命日志,指出服務(wù)器無法啟動。
最后,還有一件非常關(guān)鍵的事情需要注意:我們需要在代碼中為 lib/pq
(PostgreSQL 數(shù)據(jù)庫驅(qū)動程序)添加一個空白導(dǎo)入。沒有這個導(dǎo)入,我們的代碼將無法與數(shù)據(jù)庫進(jìn)行通信。
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)
}
}
現(xiàn)在,我們的服務(wù)器主條目已經(jīng)設(shè)置完畢。接下來,我們將在 Makefile
中添加一個新的命令來運行它。
我將這個命令命名為 server
。執(zhí)行 make server
時,它將啟動我們的服務(wù)器。同時,我們也將把這個命令添加到偽目標(biāo)(phony target)列表中。
...
server:
go run main.go
.PHONY: postgres createdb dropdb migrateup migratedown sqlc test server
然后打開終端并運行:
make server
服務(wù)器已經(jīng)啟動并在端口 8080 上監(jiān)聽,準(zhǔn)備處理 HTTP 請求。
現(xiàn)在,我將利用Postman使用教程 來測試創(chuàng)建賬戶的 API。
首先,我會添加一個新的請求,選擇 POST 方法,并輸入請求的 URL,即 http://localhost:8080/accounts
。
由于參數(shù)需要通過 JSON 主體發(fā)送,因此我選擇 Body 選項卡,進(jìn)一步選擇 Raw,并指定 JSON 格式。接下來,我需要添加兩個輸入字段:一個是所有者的名字(這里我將使用我的名字作為示例),另一個是貨幣類型(這里我們選擇美元)。
{
"owner": "Quang Pham",
"currency": "USD"
}
確定,然后單擊 發(fā)送.
測試成功了。我們收到了一個 200 OK 狀態(tài)碼,以及新創(chuàng)建的賬戶對象。該賬戶具有 ID(例如 ID=1)、余額為 0,所有者名字和貨幣類型也均正確無誤。
接下來,讓我們嘗試發(fā)送一些無效數(shù)據(jù),觀察服務(wù)器的響應(yīng)。我將兩個字段(所有者和貨幣)都設(shè)置為空字符串,然后點擊發(fā)送請求。
{
"owner": "",
"currency": ""
}
這一次,服務(wù)器返回了 400 Bad Request 狀態(tài)碼,并提示字段為必需的錯誤信息。不過,這個錯誤消息由于同時包含了兩個字段的驗證錯誤,顯得有些難以閱讀。這確實是我們在未來需要改進(jìn)的地方。
接下來,我打算嘗試輸入一個無效的貨幣代碼,比如 “xyz”,看看服務(wù)器會如何響應(yīng)。
{
"owner": "Quang Pham",
"currency": "xyz"
}
這一次,服務(wù)器同樣返回了 400 Bad Request 狀態(tài)碼,但錯誤消息有所不同。它指出驗證在 oneof
標(biāo)簽上失敗,這符合我們的預(yù)期,因為我們在代碼中僅允許兩個貨幣值:USD 和 EUR。
Gin 框架確實非常出色,它僅用幾行代碼就為我們完成了所有的輸入綁定和驗證工作。此外,Gin 還以一種易于人類閱讀的格式打印了請求日志。
好的,接下來我們將添加一個API來通過ID獲取特定賬戶。這個API與創(chuàng)建賬戶的API非常相似,因此我將復(fù)制該路由語句。
func NewServer(store *db.Store) *Server {
...
router.POST("/accounts", server.createAccount)
router.GET("/accounts/:id", server.getAccount)
...
}
在這里,我們將采用POST方法,而不是GET方法。這個路徑應(yīng)該包含我們要獲取的賬戶ID,格式為/accounts/:id
。請注意,在id
前面有一個冒號,這是我們告訴Gin框架id
是一個URI參數(shù)的方式。
接下來,我們需要在getAccount
結(jié)構(gòu)上實現(xiàn)一個新的服務(wù)器處理程序。讓我們轉(zhuǎn)到account.go
文件來實現(xiàn)這個功能。與之前類似,我們聲明了一個名為getAccountRequest
的新結(jié)構(gòu)體來存儲輸入?yún)?shù)。它將包含一個類型為int64
的ID
字段。
由于ID
是一個URI參數(shù),我們不能再像以前那樣從請求體中獲取它。相反,我們使用uri
標(biāo)簽來告訴Gin框架URI參數(shù)的名稱。
type getAccountRequest struct {
ID int64 uri:"id" binding:"required,min=1"
}
我們?yōu)橘~戶 ID 添加了一個綁定條件,即它是一個必填字段,且值必須有效。具體來說,我們不希望客戶端發(fā)送無效的 ID,比如負(fù)數(shù)。為了向 Gin 框架傳達(dá)這一要求,我們可以使用 min
條件,并將其設(shè)置為 1,因為 1 是賬戶 ID 的最小可能值。
接下來,在 server.getAccount
處理程序中,我們將執(zhí)行與之前類似的操作。首先,我們聲明一個新的變量 req
,其類型為 getAccountRequest
。但這次,我們不會調(diào)用 ShouldBindJSON
方法,而是應(yīng)該調(diào)用 ShouldBindUri
方法來從 URI 中提取參數(shù)。
如果 ShouldBindUri
方法返回錯誤,我們將直接返回一個 400 Bad Request 狀態(tài)碼。如果沒有錯誤發(fā)生,我們將調(diào)用 server.store.GetAccount()
方法,并傳入 req.ID
來獲取對應(yīng)的賬戶。這個方法會返回一個 account
對象和一個可能發(fā)生的錯誤。
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
,則可能存在兩種情況:
sql.ErrNoRows
。因此,我們需要檢查錯誤是否為 sql.ErrNoRows
,如果是,則向客戶端返回 404 Not Found 狀態(tài)碼。如果一切順利,沒有出現(xiàn)任何錯誤,我們將向客戶端返回 200 OK 狀態(tài)碼以及對應(yīng)的賬戶信息。至此,我們的 getAccount
API 就開發(fā)完成了。
接下來,讓我們重新啟動服務(wù)器,并打開 Postman使用教程 進(jìn)行測試。
我們將使用 GET 方法添加一個新請求,請求的 URL 為 http://localhost:8080/accounts/1
。在 URL 的末尾添加 /1
是因為我們希望獲取 ID 為 1 的賬戶信息。現(xiàn)在,點擊“發(fā)送”按鈕進(jìn)行測試。
請求成功,我們收到了一個200 OK狀態(tài)代碼,并找到了相應(yīng)的賬戶。這個賬戶正是我們之前創(chuàng)建的。
現(xiàn)在,讓我們嘗試獲取一個不存在的賬戶。我將ID更改為100:http://localhost:8080/accounts/100,然后再次點擊“Send”(發(fā)送)。
這一次,我們收到了一個404 Not Found狀態(tài)代碼,以及一個錯誤提示:“sql no rows in result set”。這完全符合我們的預(yù)期。
讓我們再試一次,這次使用一個負(fù)數(shù)ID:http://localhost:8080/accounts/-1。
現(xiàn)在我們遇到了一個問題,收到了一個 400 Bad Request 狀態(tài)碼,錯誤消息指出驗證失敗。不過,這并不影響我們確認(rèn) getAccount
API 的整體運行狀況是良好的。
接下來,我將為您介紹如何通過分頁功能來實現(xiàn)列表賬戶 API。
考慮到數(shù)據(jù)庫中的賬戶數(shù)量可能會隨著時間的推移而大幅增長,我們不應(yīng)該在單個 API 調(diào)用中嘗試查詢并返回所有賬戶。因此,我們采用了分頁策略,將記錄分成多個小頁面,這樣客戶端每次 API 請求就只需檢索一個頁面的數(shù)據(jù)。
這個 API 與其他 API 有所不同,因為我們不會從請求正文或 URI 路徑中提取輸入?yún)?shù),而是會從查詢字符串中獲取它們。下面是一個請求示例:
我們有兩個查詢參數(shù):page_id
和 page_size
。page_id
代表我們想要獲取的頁面索引,從第1頁開始計數(shù);而 page_size
則表示每一頁中可以返回的最大記錄數(shù)量。
這兩個參數(shù)會被添加到請求 URL 的問號之后,如下所示:http://localhost:8080/accounts?page_id=1&page_size=5
。正因為它們出現(xiàn)在 URL 的這一部分,所以被稱為查詢參數(shù),以區(qū)別于像 getAccount
請求中的賬戶 ID 那樣的 URI 參數(shù)。
現(xiàn)在,讓我們回到代碼中。我們將使用相同的 GET 方法來添加一個新的路由,但這次路徑應(yīng)僅設(shè)置為 /accounts
,因為我們計劃從查詢中獲取所需的參數(shù)。處理程序的名稱應(yīng)命名為 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
文件來實現(xiàn)server.listAccount
函數(shù)。這個函數(shù)與server.getAccount
處理程序非常相似,所以我將復(fù)制它。然后,我將結(jié)構(gòu)體的名稱更改為listAccountRequest
。
這個結(jié)構(gòu)體應(yīng)該存儲兩個參數(shù):PageID
和PageSize
。請注意,我們現(xiàn)在不是從URI中獲取這些參數(shù),而是從查詢字符串中獲取,因此我們不能使用uri
標(biāo)簽。我們應(yīng)該使用form
標(biāo)簽。
type listAccountRequest struct {
PageID int32 form:"page_id" binding:"required,min=1"
PageSize int32 form:"page_size" binding:"required,min=5,max=10"
}
這兩個參數(shù)都是必需的,最小的PageID
應(yīng)該是1。對于PageSize
,我們不希望它太大或太小,所以我將其最小值設(shè)置為5條記錄,最大值設(shè)置為10條記錄。
現(xiàn)在,server.listAccount
處理函數(shù)應(yīng)該這樣實現(xiàn):
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
變量的類型應(yīng)該被定義為 listAccountRequest
。接下來,我們會使用另一個綁定函數(shù) ShouldBindQuery
來告訴 Gin 框架從查詢字符串中提取數(shù)據(jù)。
如果在數(shù)據(jù)綁定過程中發(fā)生錯誤,我們將直接返回 400 Bad Request 狀態(tài)碼。如果沒有錯誤發(fā)生,我們會調(diào)用 server.store.ListAccounts()
方法從數(shù)據(jù)庫中查詢一頁的賬戶記錄。這個方法需要一個 ListAccountsParams
結(jié)構(gòu)體作為輸入?yún)?shù),我們需要為其中的 Limit
和 Offset
字段提供值。
Limit
字段的值就是 req.PageSize
。而 Offset
字段表示數(shù)據(jù)庫應(yīng)該跳過的記錄數(shù),這個值需要通過以下公式計算得出:(req.PageID - 1) * req.PageSize
。
ListAccounts
函數(shù)會返回一個賬戶列表和一個可能發(fā)生的錯誤。如果函數(shù)返回錯誤,我們將向客戶端返回 500 Internal Server Error 狀態(tài)碼。如果一切順利,我們將向客戶端發(fā)送一個 200 OK 狀態(tài)碼以及查詢到的賬戶列表。
至此,ListAccounts
API 的開發(fā)就完成了。
接下來,讓我們重新啟動服務(wù)器,并打開 Postman使用教程來測試這個新的 API 請求。
測試很成功,但目前名單上只有一個賬戶,這是因為我們的數(shù)據(jù)庫還比較空。事實上,到目前為止,我們只創(chuàng)建了一個賬戶。為了獲取更多的隨機(jī)數(shù)據(jù),我們可以運行之前講座中編寫的數(shù)據(jù)庫測試。
? make test
好了,現(xiàn)在我們的數(shù)據(jù)庫中應(yīng)該有很多賬戶。讓我們重新發(fā)送此 API 請求。
現(xiàn)在返回的賬戶列表正好包含了5個賬戶。注意,ID為5的賬戶并未出現(xiàn)在列表中,我猜測它可能在測試過程中被刪除了。而ID為6的賬戶則出現(xiàn)在了我們的結(jié)果中。
接下來,讓我們嘗試獲取第二頁的數(shù)據(jù)。
太好了,現(xiàn)在我們成功地獲取了接下來的5個賬戶,它們的ID從7到11。這表明我們的分頁功能運行得非常順暢。
接下來,我想再嘗試一次,不過這次是為了測試一個不存在的頁面,比如第100頁。
現(xiàn)在的情況是,當(dāng)我們請求一個不存在的頁面時,服務(wù)器會返回一個 null
響應(yīng)體。雖然從技術(shù)上講這沒錯,但我更傾向于服務(wù)器在這種情況下返回一個空列表,這樣客戶端處理起來會更方便。
在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
)在未被初始化的情況下被聲明了。因此,當(dāng)沒有記錄被添加時,它的值會默認(rèn)為 nil
而不是一個空切片。
不過,在 sqlc
的最新版本(1.5.0版)中,引入了一個名為 emit_empty_slices
的新設(shè)置。這個設(shè)置可以指示 sqlc
在沒有查詢到結(jié)果時返回一個空切片,而不是 nil
。
emit_empty_slices
設(shè)置的默認(rèn)值是 false
,意味著默認(rèn)情況下,sqlc
仍然會返回 nil
。但是,如果我們將這個值設(shè)置為 true
,那么由 many
查詢返回的結(jié)果就會是一個空切片,而不是 nil
。
現(xiàn)在,讓我們將這個新設(shè)置添加到我們的 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
您可以通過運行以下命令來檢查當(dāng)前版本:
? sqlc version
v1.5.0
對我來說,它已經(jīng)是最新版本:1.5.0
,所以現(xiàn)在我要重新生成代碼:
? make sqlc
回到Visual Studio代碼。現(xiàn)在在account.sql.go
文件中,我們可以看到items變量現(xiàn)在被初始化為一個空切片:
func (q *Queries) ListAccounts(ctx context.Context, arg ListAccountsParams) ([]Account, error) {
...
items := []Account{}
...
}
太棒了!我們重新啟動了服務(wù)器,并在Postman使用教程上進(jìn)行了測試。現(xiàn)在,當(dāng)我發(fā)送請求時,如果請求的是不存在的頁面或沒有記錄可返回,服務(wù)器會按預(yù)期返回一個空列表,而不是之前的null
響應(yīng)。
它確實奏效了!
現(xiàn)在,我想嘗試一些無效的參數(shù)來看看系統(tǒng)的反應(yīng)。比如,我將 page_size
設(shè)置為20,這個值超出了我們設(shè)定的最大約束(假設(shè)最大約束是10)。
這一次,我們收到了一個400 Bad Request狀態(tài)代碼,并且在page_size
參數(shù)上顯示了一個錯誤,提示說max
的驗證失敗了。
讓我們再試一次,使用page_size=1
。
現(xiàn)在我們?nèi)匀皇盏搅艘粋€400 Bad Request狀態(tài)代碼,但錯誤是因為page_id
驗證在required
標(biāo)記上失敗了。在驗證程序包中,任何零值都會被識別為缺失。在這種情況下,這是可以接受的,無論怎樣,我們不希望有0頁。
但是,如果您的API參數(shù)為零值,您需要注意這一點。我建議您閱讀validator
包的文檔來了解更多。
好了,今天我們已經(jīng)了解了在Go語言中使用Gin框架實現(xiàn)RESTful HTTP API是多么容易。您可以根據(jù)本教程嘗試實現(xiàn)更多路由,來自行更新或刪除賬戶。我把這個練習(xí)留給您。
非常感謝您閱讀本文。祝您編碼愉快!
原文鏈接:https://dev.to/techschoolguru/implement-restful-http-api-in-go-using-gin-4ap1