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

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

Alt Text

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

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

1、安裝 Gin

首先,請(qǐng)打開(kāi)您的瀏覽器,搜索“golang gin”,接著打開(kāi) Gin 的 Github 頁(yè)面。在頁(yè)面上向下滾動(dòng)一點(diǎn),找到“Installation”部分。

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

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

在此之后,在我們的簡(jiǎn)單銀行項(xiàng)目的go.mod文件中,我們可以看到gin作為一個(gè)新的依賴(lài)項(xiàng)與它使用的其他一些包一起添加。

Alt Text

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

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

首先,我們需要定義一個(gè)新的 Server 結(jié)構(gòu)。這個(gè)服務(wù)器將負(fù)責(zé)處理所有針對(duì)我們銀行服務(wù)的 HTTP 請(qǐng)求。它將包含兩個(gè)關(guān)鍵字段:

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

現(xiàn)在,讓我們添加一個(gè)名為 NewServer 的函數(shù)。這個(gè)函數(shù)將接收一個(gè) db.Store 作為輸入?yún)?shù),并返回一個(gè) Server 實(shí)例。此函數(shù)的職責(zé)是創(chuàng)建一個(gè)新的 Server 實(shí)例,并在這個(gè)服務(wù)器上為我們的服務(wù)配置所有的 HTTP API 路由。

首先,我們會(huì)使用傳入的 db.Store 來(lái)初始化 Server 實(shí)例中的 store 字段。接著,通過(guò)調(diào)用 gin.Default() 方法來(lái)創(chuàng)建一個(gè)新的路由器實(shí)例。雖然我們現(xiàn)在還不會(huì)為 router 添加具體的路由,但在這一步之后,我們會(huì)將 router 對(duì)象賦值給 server.router,并最終返回這個(gè) Server 實(shí)例。

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)在,讓我們添加第一個(gè) API 路由,用于創(chuàng)建一個(gè)新賬戶(hù)。由于這個(gè)操作需要使用 POST 方法,因此我們會(huì)調(diào)用 router.POST 方法進(jìn)行設(shè)置。

我們必須傳入路由的路徑,在本例中為/accounts,然后傳入一個(gè)或多個(gè)處理函數(shù)。如果傳入多個(gè)函數(shù),那么最后一個(gè)應(yīng)該是真實(shí)的處理程序,其他所有函數(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
}

目前,我們還沒(méi)有配置任何中間件,所以只需傳入一個(gè)處理程序函數(shù):server.createAccount。這是 Server 結(jié)構(gòu)體中需要我們實(shí)現(xiàn)的一個(gè)方法。該方法需要作為 Server 結(jié)構(gòu)體的方法,因?yàn)闉榱藢⑿沦~戶(hù)保存到數(shù)據(jù)庫(kù)中,我們必須訪問(wèn) store 對(duì)象。

3、實(shí)施創(chuàng)建賬戶(hù) API

接下來(lái),我將在 server 文件夾內(nèi)新建一個(gè)文件 account.go,并在其中實(shí)現(xiàn)這個(gè) API 方法。我們會(huì)聲明一個(gè)帶有 Server 指針接收器的函數(shù),函數(shù)名為 createAccount。這個(gè)函數(shù)將接收一個(gè) gin.Context 對(duì)象作為參數(shù)。

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

為什么它有這個(gè)函數(shù)簽名?讓我們看看Gin的這個(gè)router.POST函數(shù):

Alt Text

在這里,我們可以看到 HandlerFunc 被定義為一個(gè)接收 Context 作為輸入的函數(shù)?;旧?,在使用 Gin 框架時(shí),我們的處理程序中所進(jìn)行的所有操作都會(huì)涉及到這個(gè) Context 對(duì)象。它提供了許多便捷的方法,用于讀取輸入?yún)?shù)和發(fā)送響應(yīng)。

接下來(lái),讓我們聲明一個(gè)新的結(jié)構(gòu)體,用于存儲(chǔ)創(chuàng)建賬戶(hù)請(qǐng)求的數(shù)據(jù)。這個(gè)結(jié)構(gòu)體將包含幾個(gè)字段,這些字段與我們?cè)谏弦还?jié)課中在數(shù)據(jù)庫(kù)中使用的 createAccountParams 結(jié)構(gòu)體以及在 account.sql.go 文件中定義的字段類(lèi)似。

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

因此,我會(huì)復(fù)制這些字段并將它們粘貼到我們的 createAccountRequest 結(jié)構(gòu)體中??紤]到在創(chuàng)建一個(gè)新賬戶(hù)時(shí),其初始余額應(yīng)該始終設(shè)置為0,所以我們可以省略 balance 字段。我們僅允許用戶(hù)指定賬戶(hù)的所有者姓名和貨幣類(lèi)型。這些輸入?yún)?shù)將從 HTTP 請(qǐng)求的主體中獲取,該主體是一個(gè) JSON 對(duì)象,因此我會(huì)保留這些字段的 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)從客戶(hù)端接收輸入數(shù)據(jù)時(shí),進(jìn)行驗(yàn)證總是一個(gè)明智的選擇,因?yàn)榭蛻?hù)端可能會(huì)發(fā)送一些無(wú)效或我們不希望存儲(chǔ)在數(shù)據(jù)庫(kù)中的數(shù)據(jù)。

幸運(yùn)的是,Gin 框架內(nèi)部使用了一個(gè)驗(yàn)證包來(lái)自動(dòng)執(zhí)行數(shù)據(jù)驗(yàn)證。例如,我們可以利用 binding 標(biāo)簽來(lái)告知 Gin 某個(gè)字段是必需的。隨后,通過(guò)調(diào)用 ShouldBindJSON 函數(shù)來(lái)解析 HTTP 請(qǐng)求體中的輸入數(shù)據(jù),Gin 會(huì)自動(dòng)驗(yàn)證輸出對(duì)象,以確保其滿(mǎn)足我們?cè)?nbsp;binding 標(biāo)簽中指定的條件。

我將為 owner 和 currency 字段添加 binding:"required" 標(biāo)簽。此外,假設(shè)我們的銀行當(dāng)前僅支持兩種貨幣:USD 和 EUR。為了實(shí)現(xiàn)這一限制條件,我們可以使用 oneof 驗(yàn)證規(guī)則。

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

我們使用逗號(hào)來(lái)分隔多個(gè)驗(yàn)證條件,而對(duì)于 oneof 條件,則使用空格來(lái)分隔其可能的值。

現(xiàn)在,在 createAccount 函數(shù)中,我們首先聲明了一個(gè)名為 req 的變量,其類(lèi)型為 createAccountRequest。接著,我們調(diào)用 ctx.ShouldBindJSON() 函數(shù),并將 req 對(duì)象作為參數(shù)傳入。此函數(shù)會(huì)返回一個(gè)錯(cuò)誤對(duì)象。

如果返回的錯(cuò)誤對(duì)象不是 nil,則表示客戶(hù)端提供了無(wú)效的數(shù)據(jù)。在這種情況下,我們應(yīng)該向客戶(hù)端發(fā)送一個(gè) 400 Bad Request 響應(yīng)。為此,我們只需調(diào)用 ctx.JSON() 函數(shù)來(lái)發(fā)送 JSON 格式的響應(yīng)。

ctx.JSON() 函數(shù)的第一個(gè)參數(shù)是 HTTP 狀態(tài)碼,在本例中應(yīng)為 http.StatusBadRequest。第二個(gè)參數(shù)是我們希望發(fā)送給客戶(hù)端的 JSON 對(duì)象。在這里,我們只需要發(fā)送錯(cuò)誤信息,因此我們需要一個(gè)函數(shù)來(lái)將錯(cuò)誤轉(zhuǎn)換為鍵值對(duì)形式的對(duì)象,以便 Gin 可以在將其返回給客戶(hù)端之前將其序列化為 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ù),它不僅限于賬戶(hù)處理程序,也可以應(yīng)用于其他處理程序。因此,我計(jì)劃在 server.go 文件中實(shí)現(xiàn)這個(gè)函數(shù)。

errorResponse 函數(shù)將接收一個(gè)錯(cuò)誤作為輸入?yún)?shù),并返回一個(gè) gin.H 對(duì)象。gin.H 實(shí)際上是 map[string]interface{} 的一個(gè)便捷類(lèi)型,允許我們?cè)谄渲写鎯?chǔ)任意類(lèi)型的鍵值數(shù)據(jù)。

現(xiàn)在,我們先實(shí)現(xiàn)一個(gè)基本版本,該函數(shù)僅返回一個(gè)包含單個(gè)鍵 error 的 gin.H 對(duì)象,其值為錯(cuò)誤消息。未來(lái),我們可能會(huì)根據(jù)錯(cuò)誤類(lèi)型進(jìn)行更細(xì)致的處理,并將其轉(zhuǎn)換為更合適的格式。

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

現(xiàn)在,讓我們?cè)俅侮P(guān)注 createAccount 處理程序。如果輸入數(shù)據(jù)有效,那么 ShouldBindJSON 函數(shù)將不會(huì)返回錯(cuò)誤。接下來(lái),我們需要在數(shù)據(jù)庫(kù)中插入一個(gè)新的賬戶(hù)。

首先,我們聲明一個(gè) AccountParams 對(duì)象,并設(shè)置其 Owner 字段為 req.Owner,Currency 字段為 req.Currency,而 Balance 字段則設(shè)置為 0。然后,我們調(diào)用 server.store.CreateAccount 方法,傳入當(dāng)前的上下文(ctx)和剛才創(chuàng)建的 AccountParams 對(duì)象。這個(gè)方法會(huì)返回新創(chuàng)建的賬戶(hù)實(shí)例以及一個(gè)可能發(fā)生的錯(cuò)誤。

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 方法返回的錯(cuò)誤不是 nil,這意味著在嘗試將數(shù)據(jù)插入數(shù)據(jù)庫(kù)時(shí)遇到了內(nèi)部問(wèn)題。因此,我們將向客戶(hù)端返回一個(gè) 500 Internal Server Error 狀態(tài)碼,并重用 errorResponse 函數(shù)來(lái)將錯(cuò)誤信息發(fā)送給客戶(hù)端,然后立即結(jié)束處理。

如果 CreateAccount 方法沒(méi)有返回錯(cuò)誤,那么賬戶(hù)就成功創(chuàng)建了。此時(shí),我們只需向客戶(hù)端發(fā)送一個(gè) 200 OK 狀態(tài)碼,以及新創(chuàng)建的賬戶(hù)對(duì)象。至此,createAccount 處理程序就完成了。

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

接下來(lái),我們需要添加更多代碼來(lái)啟動(dòng) HTTP 服務(wù)器。為此,我們將在 Server 結(jié)構(gòu)體中添加一個(gè)新的方法 Start。這個(gè)方法將接收一個(gè) address 字符串作為輸入?yún)?shù),并返回一個(gè)錯(cuò)誤對(duì)象。Start 方法的作用是在指定的 address 上啟動(dòng) HTTP 服務(wù)器,以便開(kāi)始監(jiān)聽(tīng)并處理 API 請(qǐng)求。

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

Gin 框架已經(jīng)在路由器中內(nèi)置了啟動(dòng)服務(wù)器的功能,所以我們只需要調(diào)用 server.router.Run() 方法,并傳入服務(wù)器地址即可。

值得注意的是,server.router 字段是私有的,這意味著它不能從 api 包的外部被直接訪問(wèn)。這正是我們提供公共 Start() 函數(shù)的原因之一。目前,這個(gè)函數(shù)只包含啟動(dòng)服務(wù)器的命令,但未來(lái)我們可能會(huì)在其中添加優(yōu)雅的關(guān)閉邏輯等功能。

現(xiàn)在,讓我們?cè)诖鎯?chǔ)庫(kù)的根目錄下創(chuàng)建一個(gè) main.go 文件,作為我們服務(wù)器的入口點(diǎn)。該文件的包名應(yīng)為 main,并且包含一個(gè) main() 函數(shù)。

為了創(chuàng)建 Server 實(shí)例,我們需要先連接到數(shù)據(jù)庫(kù),并創(chuàng)建一個(gè) Store。這個(gè)過(guò)程與我們之前在 main_test.go 文件中編寫(xiě)的代碼非常相似。

因此,我會(huì)將用于數(shù)據(jù)庫(kù)連接的常量 dbDriver 和 dbSource 復(fù)制到 main.go 文件的頂部。接著,將建立數(shù)據(jù)庫(kù)連接的代碼塊也復(fù)制到 main() 函數(shù)中。

使用這個(gè)數(shù)據(jù)庫(kù)連接,我們可以通過(guò) db.NewStore() 函數(shù)創(chuàng)建一個(gè)新的 Store 實(shí)例。然后,通過(guò)調(diào)用 api.NewServer() 并傳入這個(gè) Store 實(shí)例,來(lái)創(chuàng)建一個(gè)新的服務(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)

...
}

為了啟動(dòng)服務(wù)器,我們只需調(diào)用 server.Start() 方法,并傳入服務(wù)器地址。現(xiàn)在,我將服務(wù)器地址聲明為一個(gè)常量,即 localhost 的 8080 端口。未來(lái),我們計(jì)劃重構(gòu)代碼,以便從環(huán)境變量或配置文件中加載這些配置。如果服務(wù)器啟動(dòng)過(guò)程中遇到錯(cuò)誤,我們將記錄一條致命日志,指出服務(wù)器無(wú)法啟動(dòng)。

最后,還有一件非常關(guān)鍵的事情需要注意:我們需要在代碼中為 lib/pq(PostgreSQL 數(shù)據(jù)庫(kù)驅(qū)動(dòng)程序)添加一個(gè)空白導(dǎo)入。沒(méi)有這個(gè)導(dǎo)入,我們的代碼將無(wú)法與數(shù)據(jù)庫(kù)進(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è)置完畢。接下來(lái),我們將在 Makefile 中添加一個(gè)新的命令來(lái)運(yùn)行它。

我將這個(gè)命令命名為 server。執(zhí)行 make server 時(shí),它將啟動(dòng)我們的服務(wù)器。同時(shí),我們也將把這個(gè)命令添加到偽目標(biāo)(phony target)列表中。

...

server:
go run main.go

.PHONY: postgres createdb dropdb migrateup migratedown sqlc test server

然后打開(kāi)終端并運(yùn)行:

make server
Alt Text

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

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

現(xiàn)在,我將利用Postman使用教程 來(lái)測(cè)試創(chuàng)建賬戶(hù)的 API。

首先,我會(huì)添加一個(gè)新的請(qǐng)求,選擇 POST 方法,并輸入請(qǐng)求的 URL,即 http://localhost:8080/accounts。

由于參數(shù)需要通過(guò) JSON 主體發(fā)送,因此我選擇 Body 選項(xiàng)卡,進(jìn)一步選擇 Raw,并指定 JSON 格式。接下來(lái),我需要添加兩個(gè)輸入字段:一個(gè)是所有者的名字(這里我將使用我的名字作為示例),另一個(gè)是貨幣類(lèi)型(這里我們選擇美元)。

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

確定,然后單擊 發(fā)送.

Alt Text

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

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

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

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

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

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

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

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

Alt Text

6、實(shí)施獲取帳戶(hù) API

好的,接下來(lái)我們將添加一個(gè)API來(lái)通過(guò)ID獲取特定賬戶(hù)。這個(gè)API與創(chuàng)建賬戶(hù)的API非常相似,因此我將復(fù)制該路由語(yǔ)句。

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

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

...
}

在這里,我們將采用POST方法,而不是GET方法。這個(gè)路徑應(yīng)該包含我們要獲取的賬戶(hù)ID,格式為/accounts/:id。請(qǐng)注意,在id前面有一個(gè)冒號(hào),這是我們告訴Gin框架id是一個(gè)URI參數(shù)的方式。

接下來(lái),我們需要在getAccount結(jié)構(gòu)上實(shí)現(xiàn)一個(gè)新的服務(wù)器處理程序。讓我們轉(zhuǎn)到account.go文件來(lái)實(shí)現(xiàn)這個(gè)功能。與之前類(lèi)似,我們聲明了一個(gè)名為getAccountRequest的新結(jié)構(gòu)體來(lái)存儲(chǔ)輸入?yún)?shù)。它將包含一個(gè)類(lèi)型為int64ID字段。

由于ID是一個(gè)URI參數(shù),我們不能再像以前那樣從請(qǐng)求體中獲取它。相反,我們使用uri標(biāo)簽來(lái)告訴Gin框架URI參數(shù)的名稱(chēng)。

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

我們?yōu)橘~戶(hù) ID 添加了一個(gè)綁定條件,即它是一個(gè)必填字段,且值必須有效。具體來(lái)說(shuō),我們不希望客戶(hù)端發(fā)送無(wú)效的 ID,比如負(fù)數(shù)。為了向 Gin 框架傳達(dá)這一要求,我們可以使用 min 條件,并將其設(shè)置為 1,因?yàn)?1 是賬戶(hù) ID 的最小可能值。

接下來(lái),在 server.getAccount 處理程序中,我們將執(zhí)行與之前類(lèi)似的操作。首先,我們聲明一個(gè)新的變量 req,其類(lèi)型為 getAccountRequest。但這次,我們不會(huì)調(diào)用 ShouldBindJSON 方法,而是應(yīng)該調(diào)用 ShouldBindUri 方法來(lái)從 URI 中提取參數(shù)。

如果 ShouldBindUri 方法返回錯(cuò)誤,我們將直接返回一個(gè) 400 Bad Request 狀態(tài)碼。如果沒(méi)有錯(cuò)誤發(fā)生,我們將調(diào)用 server.store.GetAccount() 方法,并傳入 req.ID 來(lái)獲取對(duì)應(yīng)的賬戶(hù)。這個(gè)方法會(huì)返回一個(gè) account 對(duì)象和一個(gè)可能發(fā)生的錯(cuò)誤。

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() 方法返回的錯(cuò)誤不為 nil,則可能存在兩種情況:

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

7、使用 Postman使用教程 測(cè)試獲取帳戶(hù) API

接下來(lái),讓我們重新啟動(dòng)服務(wù)器,并打開(kāi) Postman使用教程 進(jìn)行測(cè)試。

我們將使用 GET 方法添加一個(gè)新請(qǐng)求,請(qǐng)求的 URL 為 http://localhost:8080/accounts/1。在 URL 的末尾添加 /1 是因?yàn)槲覀兿M@取 ID 為 1 的賬戶(hù)信息?,F(xiàn)在,點(diǎn)擊“發(fā)送”按鈕進(jìn)行測(cè)試。

Alt Text

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

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

Alt Text

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

讓我們?cè)僭囈淮?,這次使用一個(gè)負(fù)數(shù)ID:http://localhost:8080/accounts/-1。

Alt Text

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

8、實(shí)施列表賬戶(hù) API

接下來(lái),我將為您介紹如何通過(guò)分頁(yè)功能來(lái)實(shí)現(xiàn)列表賬戶(hù) API。

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

這個(gè) API 與其他 API 有所不同,因?yàn)槲覀儾粫?huì)從請(qǐng)求正文或 URI 路徑中提取輸入?yún)?shù),而是會(huì)從查詢(xún)字符串中獲取它們。下面是一個(gè)請(qǐng)求示例:

Alt Text

我們有兩個(gè)查詢(xún)參數(shù):page_id 和 page_size。page_id 代表我們想要獲取的頁(yè)面索引,從第1頁(yè)開(kāi)始計(jì)數(shù);而 page_size 則表示每一頁(yè)中可以返回的最大記錄數(shù)量。

這兩個(gè)參數(shù)會(huì)被添加到請(qǐng)求 URL 的問(wèn)號(hào)之后,如下所示:http://localhost:8080/accounts?page_id=1&page_size=5。正因?yàn)樗鼈兂霈F(xiàn)在 URL 的這一部分,所以被稱(chēng)為查詢(xún)參數(shù),以區(qū)別于像 getAccount 請(qǐng)求中的賬戶(hù) ID 那樣的 URI 參數(shù)。

現(xiàn)在,讓我們回到代碼中。我們將使用相同的 GET 方法來(lái)添加一個(gè)新的路由,但這次路徑應(yīng)僅設(shè)置為 /accounts,因?yàn)槲覀冇?jì)劃從查詢(xún)中獲取所需的參數(shù)。處理程序的名稱(chēng)應(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
}

好的,讓我們打開(kāi)account.go文件來(lái)實(shí)現(xiàn)server.listAccount函數(shù)。這個(gè)函數(shù)與server.getAccount處理程序非常相似,所以我將復(fù)制它。然后,我將結(jié)構(gòu)體的名稱(chēng)更改為listAccountRequest。

這個(gè)結(jié)構(gòu)體應(yīng)該存儲(chǔ)兩個(gè)參數(shù):PageIDPageSize。請(qǐng)注意,我們現(xiàn)在不是從URI中獲取這些參數(shù),而是從查詢(xún)字符串中獲取,因此我們不能使用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" }

這兩個(gè)參數(shù)都是必需的,最小的PageID應(yīng)該是1。對(duì)于PageSize,我們不希望它太大或太小,所以我將其最小值設(shè)置為5條記錄,最大值設(shè)置為10條記錄。

現(xiàn)在,server.listAccount處理函數(shù)應(yīng)該這樣實(shí)現(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 變量的類(lèi)型應(yīng)該被定義為 listAccountRequest。接下來(lái),我們會(huì)使用另一個(gè)綁定函數(shù) ShouldBindQuery 來(lái)告訴 Gin 框架從查詢(xún)字符串中提取數(shù)據(jù)。

如果在數(shù)據(jù)綁定過(guò)程中發(fā)生錯(cuò)誤,我們將直接返回 400 Bad Request 狀態(tài)碼。如果沒(méi)有錯(cuò)誤發(fā)生,我們會(huì)調(diào)用 server.store.ListAccounts() 方法從數(shù)據(jù)庫(kù)中查詢(xún)一頁(yè)的賬戶(hù)記錄。這個(gè)方法需要一個(gè) ListAccountsParams 結(jié)構(gòu)體作為輸入?yún)?shù),我們需要為其中的 Limit 和 Offset 字段提供值。

Limit 字段的值就是 req.PageSize。而 Offset 字段表示數(shù)據(jù)庫(kù)應(yīng)該跳過(guò)的記錄數(shù),這個(gè)值需要通過(guò)以下公式計(jì)算得出:(req.PageID - 1) * req.PageSize。

ListAccounts 函數(shù)會(huì)返回一個(gè)賬戶(hù)列表和一個(gè)可能發(fā)生的錯(cuò)誤。如果函數(shù)返回錯(cuò)誤,我們將向客戶(hù)端返回 500 Internal Server Error 狀態(tài)碼。如果一切順利,我們將向客戶(hù)端發(fā)送一個(gè) 200 OK 狀態(tài)碼以及查詢(xún)到的賬戶(hù)列表。

至此,ListAccounts API 的開(kāi)發(fā)就完成了。

9、使用 Postman使用教程 測(cè)試列表賬戶(hù) API

接下來(lái),讓我們重新啟動(dòng)服務(wù)器,并打開(kāi) Postman使用教程來(lái)測(cè)試這個(gè)新的 API 請(qǐng)求。

Alt Text

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

? make test

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

Alt Text

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

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

Alt Text

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

接下來(lái),我想再?lài)L試一次,不過(guò)這次是為了測(cè)試一個(gè)不存在的頁(yè)面,比如第100頁(yè)。

Alt Text

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

返回空列表而不是 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 變量(類(lèi)型為 []Account)在未被初始化的情況下被聲明了。因此,當(dāng)沒(méi)有記錄被添加時(shí),它的值會(huì)默認(rèn)為 nil 而不是一個(gè)空切片。

不過(guò),在 sqlc 的最新版本(1.5.0版)中,引入了一個(gè)名為 emit_empty_slices 的新設(shè)置。這個(gè)設(shè)置可以指示 sqlc 在沒(méi)有查詢(xún)到結(jié)果時(shí)返回一個(gè)空切片,而不是 nil

emit_empty_slices 設(shè)置的默認(rèn)值是 false,意味著默認(rèn)情況下,sqlc 仍然會(huì)返回 nil。但是,如果我們將這個(gè)值設(shè)置為 true,那么由 many 查詢(xún)返回的結(jié)果就會(huì)是一個(gè)空切片,而不是 nil

現(xiàn)在,讓我們將這個(gè)新設(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

保存它,然后打開(kāi)終端將 sqlc 升級(jí)到最新版本。如果您使用的是 Mac 并使用 Homebrew,只需運(yùn)行:

? brew upgrade sqlc

您可以通過(guò)運(yùn)行以下命令來(lái)檢查當(dāng)前版本:

? sqlc version
v1.5.0

對(duì)我來(lái)說(shuō),它已經(jīng)是最新版本:1.5.0,所以現(xiàn)在我要重新生成代碼:

? make sqlc

回到Visual Studio代碼。現(xiàn)在在account.sql.go文件中,我們可以看到items變量現(xiàn)在被初始化為一個(gè)空切片:

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

items := []Account{}

...
}

太棒了!我們重新啟動(dòng)了服務(wù)器,并在Postman使用教程上進(jìn)行了測(cè)試?,F(xiàn)在,當(dāng)我發(fā)送請(qǐng)求時(shí),如果請(qǐng)求的是不存在的頁(yè)面或沒(méi)有記錄可返回,服務(wù)器會(huì)按預(yù)期返回一個(gè)空列表,而不是之前的null響應(yīng)。

Alt Text

它確實(shí)奏效了!

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

Alt Text

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

讓我們?cè)僭囈淮危褂?code>page_size=1。

Alt Text

現(xiàn)在我們?nèi)匀皇盏搅艘粋€(gè)400 Bad Request狀態(tài)代碼,但錯(cuò)誤是因?yàn)?code>page_id驗(yàn)證在required標(biāo)記上失敗了。在驗(yàn)證程序包中,任何零值都會(huì)被識(shí)別為缺失。在這種情況下,這是可以接受的,無(wú)論怎樣,我們不希望有0頁(yè)。

但是,如果您的API參數(shù)為零值,您需要注意這一點(diǎn)。我建議您閱讀validator包的文檔來(lái)了解更多。

好了,今天我們已經(jīng)了解了在Go語(yǔ)言中使用Gin框架實(shí)現(xiàn)RESTful HTTP API是多么容易。您可以根據(jù)本教程嘗試實(shí)現(xiàn)更多路由,來(lái)自行更新或刪除賬戶(hù)。我把這個(gè)練習(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ù)商零注冊(cè)

多API并行試用

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

查看全部API→
??

熱門(mén)場(chǎng)景實(shí)測(cè),選對(duì)API

#AI文本生成大模型API

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

25個(gè)渠道
一鍵對(duì)比試用API 限時(shí)免費(fèi)

#AI深度推理大模型API

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

10個(gè)渠道
一鍵對(duì)比試用API 限時(shí)免費(fèi)