它們提供了豐富的功能,例如路由、參數(shù)綁定、數(shù)據(jù)驗證以及中間件等,其中一些框架甚至內(nèi)置了 ORM(對象關(guān)系映射)功能。

如果您更傾向于選擇僅具備路由功能的輕量級軟件包,那么以下是一些備受歡迎的 golang HTTP 路由器選項:

Alt Text

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

二、Gin框架實現(xiàn)操作指南

1、安裝 Gin

首先,請打開您的瀏覽器,搜索“golang gin”,接著打開 Gin 的 Github 頁面。在頁面上向下滾動一點,找到“Installation”部分。

接下來,復(fù)制提供的 go get 命令,并在您的終端中執(zhí)行該命令以安裝 Gin 軟件包。

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

在此之后,在我們的簡單銀行項目的go.mod文件中,我們可以看到gin作為一個新的依賴項與它使用的其他一些包一起添加。

Alt Text

2、定義服務(wù)器結(jié)構(gòu)

現(xiàn)在,我將創(chuàng)建一個名為 api 的新文件夾,并在其中新建一個文件 server.go。這個文件將作為我們實現(xiàn) HTTP API 服務(wù)器的主要場所。

首先,我們需要定義一個新的 Server 結(jié)構(gòu)。這個服務(wù)器將負(fù)責(zé)處理所有針對我們銀行服務(wù)的 HTTP 請求。它將包含兩個關(guā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 對象。

3、實施創(chuàng)建賬戶 API

接下來,我將在 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ù):

Alt Text

在這里,我們可以看到 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.OwnerCurrency 字段為 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 處理程序就完成了。

4、啟動 HTTP 服務(wù)器

接下來,我們需要添加更多代碼來啟動 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
Alt Text

服務(wù)器已經(jīng)啟動并在端口 8080 上監(jiān)聽,準(zhǔn)備處理 HTTP 請求。

5、使用 Postman使用教程 測試創(chuàng)建帳戶 API

現(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ā)送.

Alt Text

測試成功了。我們收到了一個 200 OK 狀態(tài)碼,以及新創(chuàng)建的賬戶對象。該賬戶具有 ID(例如 ID=1)、余額為 0,所有者名字和貨幣類型也均正確無誤。

接下來,讓我們嘗試發(fā)送一些無效數(shù)據(jù),觀察服務(wù)器的響應(yīng)。我將兩個字段(所有者和貨幣)都設(shè)置為空字符串,然后點擊發(fā)送請求。

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

這一次,服務(wù)器返回了 400 Bad Request 狀態(tài)碼,并提示字段為必需的錯誤信息。不過,這個錯誤消息由于同時包含了兩個字段的驗證錯誤,顯得有些難以閱讀。這確實是我們在未來需要改進(jìn)的地方。

接下來,我打算嘗試輸入一個無效的貨幣代碼,比如 “xyz”,看看服務(wù)器會如何響應(yīng)。

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

這一次,服務(wù)器同樣返回了 400 Bad Request 狀態(tài)碼,但錯誤消息有所不同。它指出驗證在 oneof 標(biāo)簽上失敗,這符合我們的預(yù)期,因為我們在代碼中僅允許兩個貨幣值:USD 和 EUR。

Gin 框架確實非常出色,它僅用幾行代碼就為我們完成了所有的輸入綁定和驗證工作。此外,Gin 還以一種易于人類閱讀的格式打印了請求日志。

Alt Text

6、實施獲取帳戶 API

好的,接下來我們將添加一個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ù)。它將包含一個類型為int64ID字段。

由于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,則可能存在兩種情況:

如果一切順利,沒有出現(xiàn)任何錯誤,我們將向客戶端返回 200 OK 狀態(tài)碼以及對應(yīng)的賬戶信息。至此,我們的 getAccount API 就開發(fā)完成了。

7、使用 Postman使用教程 測試獲取帳戶 API

接下來,讓我們重新啟動服務(wù)器,并打開 Postman使用教程 進(jìn)行測試。

我們將使用 GET 方法添加一個新請求,請求的 URL 為 http://localhost:8080/accounts/1。在 URL 的末尾添加 /1 是因為我們希望獲取 ID 為 1 的賬戶信息。現(xiàn)在,點擊“發(fā)送”按鈕進(jìn)行測試。

Alt Text

請求成功,我們收到了一個200 OK狀態(tài)代碼,并找到了相應(yīng)的賬戶。這個賬戶正是我們之前創(chuàng)建的。

現(xiàn)在,讓我們嘗試獲取一個不存在的賬戶。我將ID更改為100:http://localhost:8080/accounts/100,然后再次點擊“Send”(發(fā)送)。

Alt Text

這一次,我們收到了一個404 Not Found狀態(tài)代碼,以及一個錯誤提示:“sql no rows in result set”。這完全符合我們的預(yù)期。

讓我們再試一次,這次使用一個負(fù)數(shù)ID:http://localhost:8080/accounts/-1。

Alt Text

現(xiàn)在我們遇到了一個問題,收到了一個 400 Bad Request 狀態(tài)碼,錯誤消息指出驗證失敗。不過,這并不影響我們確認(rèn) getAccount API 的整體運行狀況是良好的。

8、實施列表賬戶 API

接下來,我將為您介紹如何通過分頁功能來實現(xiàn)列表賬戶 API。

考慮到數(shù)據(jù)庫中的賬戶數(shù)量可能會隨著時間的推移而大幅增長,我們不應(yīng)該在單個 API 調(diào)用中嘗試查詢并返回所有賬戶。因此,我們采用了分頁策略,將記錄分成多個小頁面,這樣客戶端每次 API 請求就只需檢索一個頁面的數(shù)據(jù)。

這個 API 與其他 API 有所不同,因為我們不會從請求正文或 URI 路徑中提取輸入?yún)?shù),而是會從查詢字符串中獲取它們。下面是一個請求示例:

Alt Text

我們有兩個查詢參數(shù):page_id 和 page_sizepage_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ù):PageIDPageSize。請注意,我們現(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ā)就完成了。

9、使用 Postman使用教程 測試列表賬戶 API

接下來,讓我們重新啟動服務(wù)器,并打開 Postman使用教程來測試這個新的 API 請求。

Alt Text

測試很成功,但目前名單上只有一個賬戶,這是因為我們的數(shù)據(jù)庫還比較空。事實上,到目前為止,我們只創(chuàng)建了一個賬戶。為了獲取更多的隨機(jī)數(shù)據(jù),我們可以運行之前講座中編寫的數(shù)據(jù)庫測試。

? make test

好了,現(xiàn)在我們的數(shù)據(jù)庫中應(yīng)該有很多賬戶。讓我們重新發(fā)送此 API 請求。

Alt Text

現(xiàn)在返回的賬戶列表正好包含了5個賬戶。注意,ID為5的賬戶并未出現(xiàn)在列表中,我猜測它可能在測試過程中被刪除了。而ID為6的賬戶則出現(xiàn)在了我們的結(jié)果中。

接下來,讓我們嘗試獲取第二頁的數(shù)據(jù)。

Alt Text

太好了,現(xiàn)在我們成功地獲取了接下來的5個賬戶,它們的ID從7到11。這表明我們的分頁功能運行得非常順暢。

接下來,我想再嘗試一次,不過這次是為了測試一個不存在的頁面,比如第100頁。

Alt Text

現(xiàn)在的情況是,當(dāng)我們請求一個不存在的頁面時,服務(wù)器會返回一個 null 響應(yīng)體。雖然從技術(shù)上講這沒錯,但我更傾向于服務(wù)器在這種情況下返回一個空列表,這樣客戶端處理起來會更方便。

返回空列表而不是 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)在未被初始化的情況下被聲明了。因此,當(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)。

Alt Text

它確實奏效了!

現(xiàn)在,我想嘗試一些無效的參數(shù)來看看系統(tǒng)的反應(yīng)。比如,我將 page_size 設(shè)置為20,這個值超出了我們設(shè)定的最大約束(假設(shè)最大約束是10)。

Alt Text

這一次,我們收到了一個400 Bad Request狀態(tài)代碼,并且在page_size參數(shù)上顯示了一個錯誤,提示說max的驗證失敗了。

讓我們再試一次,使用page_size=1

Alt Text

現(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

上一篇:

使用Flask、Google Cloud SQL和App Engine設(shè)置API

下一篇:

使用Django REST Framework構(gòu)建API
#你可能也喜歡這些API文章!

我們有何不同?

API服務(wù)商零注冊

多API并行試用

數(shù)據(jù)驅(qū)動選型,提升決策效率

查看全部API→
??

熱門場景實測,選對API

#AI文本生成大模型API

對比大模型API的內(nèi)容創(chuàng)意新穎性、情感共鳴力、商業(yè)轉(zhuǎn)化潛力

25個渠道
一鍵對比試用API 限時免費

#AI深度推理大模型API

對比大模型API的邏輯推理準(zhǔn)確性、分析深度、可視化建議合理性

10個渠道
一鍵對比試用API 限時免費