教程:使用 Go 和 Gin 開發 RESTful API
本教程將介紹使用 Go 和 Gin Web Framework (Gin) 編寫 RESTful Web 服務 API 的基礎知識。
如果您對 Go 及其工具鏈有基本的瞭解,將能從本教程中獲得最大收益。如果您是第一次接觸 Go,請參閱 教程:Go 入門 以快速瞭解。
Gin 簡化了構建 Web 應用程式(包括 Web 服務)的許多編碼任務。在本教程中,您將使用 Gin 來路由請求、檢索請求詳細資訊以及為響應進行 JSON 編組。
在本教程中,您將構建一個具有兩個端點的 RESTful API 伺服器。您的示例專案將是一個關於老式爵士唱片資料的儲存庫。
本教程包含以下章節
- 設計 API 端點。
- 為您的程式碼建立一個資料夾。
- 建立資料。
- 編寫一個返回所有項的處理器。
- 編寫一個新增新項的處理器。
- 編寫一個返回特定項的處理器。
注意: 有關其他教程,請參閱教程。
要以互動式教程的形式完成此操作,您可以在 Google Cloud Shell 中完成,請點選下方按鈕。
先決條件
- 安裝 Go 1.16 或更高版本。 有關安裝說明,請參閱 安裝 Go。
- 一個程式碼編輯工具。 任何文字編輯器都可以。
- 命令終端。 Go 在 Linux 和 Mac 上的任何終端以及 Windows 上的 PowerShell 或 cmd 中都能很好地工作。
- curl 工具。 在 Linux 和 Mac 上,此工具應已預裝。在 Windows 上,它包含在 Windows 10 內部版本 17063 及更高版本中。對於早期版本的 Windows,您可能需要安裝它。更多資訊,請參閱 Tar 和 Curl 在 Windows 上可用。
設計 API 端點
您將構建一個提供對銷售老式黑膠唱片商店的訪問的 API。因此,您需要提供客戶端可以獲取和新增使用者專輯的端點。
開發 API 時,通常從設計端點開始。API 使用者會更容易理解端點,從而提高成功率。
以下是本教程中將建立的端點。
/albums
GET
– 獲取所有專輯的列表,以 JSON 格式返回。POST
– 從作為 JSON 傳送的請求資料中新增新專輯。
/albums/:id
GET
– 按 ID 獲取專輯,以 JSON 格式返回專輯資料。
接下來,您將建立一個資料夾來存放您的程式碼。
為您的程式碼建立一個資料夾
首先,建立一個專案來存放您將編寫的程式碼。
-
開啟命令提示符並切換到您的主目錄。
在 Linux 或 Mac 上
$ cd
在 Windows 上
C:\> cd %HOMEPATH%
-
使用命令提示符,建立一個名為 web-service-gin 的程式碼目錄。
$ mkdir web-service-gin $ cd web-service-gin
-
建立一個模組來管理依賴項。
執行
go mod init
命令,並指定您的程式碼所在的模組路徑。$ go mod init example/web-service-gin go: creating new go.mod: module example/web-service-gin
此命令會建立一個 go.mod 檔案,其中將列出您新增的依賴項以便跟蹤。有關使用模組路徑命名模組的更多資訊,請參閱 管理依賴項。
接下來,您將設計用於處理資料的結構體。
建立資料
為使教程保持簡單,您將在記憶體中儲存資料。更典型的 API 會與資料庫進行互動。
請注意,將資料儲存在記憶體中意味著每次停止伺服器時,專輯集合都將丟失,並在啟動時重新建立。
編寫程式碼
-
使用文字編輯器,在 web-service 目錄中建立一個名為 main.go 的檔案。您將在該檔案中編寫 Go 程式碼。
-
在 main.go 檔案的頂部,貼上以下包宣告。
package main
獨立程式(與庫相對)始終位於 `main` 包中。
-
在 package 宣告下方,貼上以下
album
結構體的宣告。您將使用它來在記憶體中儲存專輯資料。Struct 標籤,如
json:"artist"
,指定當結構體內容序列化為 JSON 時欄位的名稱。如果沒有這些標籤,JSON 將使用結構體中大寫的欄位名稱——這在 JSON 中不太常見。// album represents data about a record album. type album struct { ID string `json:"id"` Title string `json:"title"` Artist string `json:"artist"` Price float64 `json:"price"` }
-
在您剛剛新增的結構體宣告下方,貼上以下
album
結構體切片,其中包含您將用於開始的資料。// albums slice to seed record album data. var albums = []album{ {ID: "1", Title: "Blue Train", Artist: "John Coltrane", Price: 56.99}, {ID: "2", Title: "Jeru", Artist: "Gerry Mulligan", Price: 17.99}, {ID: "3", Title: "Sarah Vaughan and Clifford Brown", Artist: "Sarah Vaughan", Price: 39.99}, }
接下來,您將編寫程式碼來實現您的第一個端點。
編寫一個返回所有項的處理器
當客戶端在 GET /albums
發出請求時,您希望以 JSON 格式返回所有專輯。
為此,您將編寫以下內容
- 準備響應的邏輯
- 將請求路徑對映到您的邏輯的程式碼
請注意,這與它們在執行時執行的順序相反,但您是先新增依賴項,然後是依賴於這些依賴項的程式碼。
編寫程式碼
-
在上一節新增的結構體程式碼下方,貼上以下程式碼以獲取專輯列表。
此
getAlbums
函式會從album
結構體切片建立 JSON,並將 JSON 寫入響應。// getAlbums responds with the list of all albums as JSON. func getAlbums(c *gin.Context) { c.IndentedJSON(http.StatusOK, albums) }
在此程式碼中,您
-
編寫一個接受
gin.Context
引數的getAlbums
函式。請注意,您可以為該函式命名為任何名稱——Gin 和 Go 都不要求特定的函式名稱格式。gin.Context
是 Gin 中最重要的部分。它承載請求詳細資訊、驗證和序列化 JSON 等。(儘管名稱相似,但它與 Go 內建的context
包不同。) -
呼叫
Context.IndentedJSON
將結構體序列化為 JSON 並將其新增到響應中。函式的第一個引數是您要傳送給客戶端的 HTTP 狀態碼。在這裡,您傳遞了
net/http
包中的StatusOK
常量,表示200 OK
。請注意,您可以將
Context.IndentedJSON
替換為呼叫Context.JSON
來發送更緊湊的 JSON。實際上,縮排形式在除錯時更容易使用,並且尺寸差異通常很小。
-
-
在 main.go 的頂部附近,緊隨
albums
切片宣告下方,貼上以下程式碼以將處理器函式分配給一個端點路徑。這會建立一個關聯,其中
getAlbums
處理對/albums
端點路徑的請求。func main() { router := gin.Default() router.GET("/albums", getAlbums) router.Run("localhost:8080") }
在此程式碼中,您
-
使用
Default
初始化一個 Gin 路由器。 -
使用
GET
函式將GET
HTTP 方法和/albums
路徑與處理器函式關聯起來。請注意,您傳遞的是
getAlbums
函式的名稱。這不同於傳遞函式的結果,後者您會透過傳遞getAlbums()
(注意括號) 來實現。 -
使用
Run
函式將路由器附加到http.Server
並啟動伺服器。
-
-
在 main.go 的頂部附近,緊隨 package 宣告下方,匯入您將需要的包來支援您剛剛編寫的程式碼。
程式碼的第一行應如下所示:
package main import ( "net/http" "github.com/gin-gonic/gin" )
-
儲存 main.go。
執行程式碼
-
開始跟蹤 Gin 模組作為依賴項。
在命令列中,使用
go get
命令將 github.com/gin-gonic/gin 模組新增為您的模組的依賴項。使用點引數表示“獲取當前目錄中程式碼的依賴項”。$ go get . go get: added github.com/gin-gonic/gin v1.7.2
Go 已解析並下載了此依賴項,以滿足您在上一步中新增的
import
宣告。 -
從包含 main.go 的目錄的命令列中執行程式碼。使用點引數表示“運行當前目錄中的程式碼”。
$ go run .
程式碼執行後,您就擁有了一個正在執行的 HTTP 伺服器,您可以向其傳送請求。
-
從一個新的命令列視窗,使用
curl
向您正在執行的 Web 服務發出請求。$ curl https://:8080/albums
該命令應顯示您為服務填充的資料。
[ { "id": "1", "title": "Blue Train", "artist": "John Coltrane", "price": 56.99 }, { "id": "2", "title": "Jeru", "artist": "Gerry Mulligan", "price": 17.99 }, { "id": "3", "title": "Sarah Vaughan and Clifford Brown", "artist": "Sarah Vaughan", "price": 39.99 } ]
您已經啟動了一個 API!在下一節中,您將建立另一個端點,並編寫程式碼來處理 POST
請求以新增一個項。
編寫一個新增新項的處理器
當客戶端在 POST /albums
發出請求時,您想將請求正文中描述的專輯新增到現有的專輯資料中。
為此,您將編寫以下內容
- 將新專輯新增到現有列表的邏輯。
- 一些程式碼將
POST
請求路由到您的邏輯。
編寫程式碼
-
新增程式碼以將專輯資料新增到專輯列表中。
在
import
語句之後,貼上以下程式碼。(檔案的末尾是放置此程式碼的好位置,但 Go 不強制要求宣告函式的順序。)// postAlbums adds an album from JSON received in the request body. func postAlbums(c *gin.Context) { var newAlbum album // Call BindJSON to bind the received JSON to // newAlbum. if err := c.BindJSON(&newAlbum); err != nil { return } // Add the new album to the slice. albums = append(albums, newAlbum) c.IndentedJSON(http.StatusCreated, newAlbum) }
在此程式碼中,您
- 使用
Context.BindJSON
將請求正文繫結到newAlbum
。 - 將從 JSON 初始化出的
album
結構體追加到albums
切片。 - 向響應新增
201
狀態碼,以及表示您新增的專輯的 JSON。
- 使用
-
修改您的
main
函式,使其包含router.POST
函式,如下所示。func main() { router := gin.Default() router.GET("/albums", getAlbums) router.POST("/albums", postAlbums) router.Run("localhost:8080") }
在此程式碼中,您
-
將
POST
方法在/albums
路徑與postAlbums
函式關聯起來。使用 Gin,您可以將處理器與 HTTP 方法和路徑的組合關聯起來。透過這種方式,您可以根據客戶端使用的方法,根據傳送到單個路徑的請求分別進行路由。
-
執行程式碼
-
如果伺服器在上一個部分仍然執行,請將其停止。
-
在包含 main.go 的目錄的命令列中,執行程式碼。
$ go run .
-
從另一個命令列視窗,使用
curl
向您正在執行的 Web 服務發出請求。$ curl https://:8080/albums \ --include \ --header "Content-Type: application/json" \ --request "POST" \ --data '{"id": "4","title": "The Modern Sound of Betty Carter","artist": "Betty Carter","price": 49.99}'
該命令應顯示已新增專輯的標頭和 JSON。
HTTP/1.1 201 Created Content-Type: application/json; charset=utf-8 Date: Wed, 02 Jun 2021 00:34:12 GMT Content-Length: 116 { "id": "4", "title": "The Modern Sound of Betty Carter", "artist": "Betty Carter", "price": 49.99 }
-
與上一節一樣,使用
curl
獲取完整的專輯列表,您可以用來確認新專輯已新增。$ curl https://:8080/albums \ --header "Content-Type: application/json" \ --request "GET"
該命令應顯示專輯列表。
[ { "id": "1", "title": "Blue Train", "artist": "John Coltrane", "price": 56.99 }, { "id": "2", "title": "Jeru", "artist": "Gerry Mulligan", "price": 17.99 }, { "id": "3", "title": "Sarah Vaughan and Clifford Brown", "artist": "Sarah Vaughan", "price": 39.99 }, { "id": "4", "title": "The Modern Sound of Betty Carter", "artist": "Betty Carter", "price": 49.99 } ]
在下一節中,您將新增程式碼來處理特定項的 GET
請求。
編寫一個返回特定項的處理器
當客戶端向 GET /albums/[id]
發出請求時,您希望返回 ID 與 id
路徑引數匹配的專輯。
為此,您將
- 新增檢索所請求專輯的邏輯。
- 將路徑對映到邏輯。
編寫程式碼
-
在上一節新增的
postAlbums
函式下方,貼上以下程式碼以檢索特定專輯。此
getAlbumByID
函式將提取請求路徑中的 ID,然後找到匹配的專輯。// getAlbumByID locates the album whose ID value matches the id // parameter sent by the client, then returns that album as a response. func getAlbumByID(c *gin.Context) { id := c.Param("id") // Loop over the list of albums, looking for // an album whose ID value matches the parameter. for _, a := range albums { if a.ID == id { c.IndentedJSON(http.StatusOK, a) return } } c.IndentedJSON(http.StatusNotFound, gin.H{"message": "album not found"}) }
在此程式碼中,您
-
使用
Context.Param
從 URL 中檢索id
路徑引數。當您將此處理器對映到路徑時,將在路徑中包含該引數的佔位符。 -
遍歷切片中的
album
結構體,查詢 ID 欄位值與id
引數值匹配的結構體。如果找到,則將該album
結構體序列化為 JSON,並以200 OK
HTTP 程式碼作為響應返回。如上所述,真實世界的服務可能使用資料庫查詢來執行此查詢。
-
如果未找到專輯,則返回 HTTP
404
錯誤,使用http.StatusNotFound
。
-
-
最後,修改您的
main
函式,使其包含一個新的router.GET
呼叫,其中路徑現在是/albums/:id
,如下例所示。func main() { router := gin.Default() router.GET("/albums", getAlbums) router.GET("/albums/:id", getAlbumByID) router.POST("/albums", postAlbums) router.Run("localhost:8080") }
在此程式碼中,您
- 將
/albums/:id
路徑與getAlbumByID
函式關聯起來。在 Gin 中,路徑中專案前面的冒號表示該專案是路徑引數。
- 將
執行程式碼
-
如果伺服器在上一個部分仍然執行,請將其停止。
-
從包含 main.go 的目錄的命令列中執行程式碼以啟動伺服器。
$ go run .
-
從另一個命令列視窗,使用
curl
向您正在執行的 Web 服務發出請求。$ curl https://:8080/albums/2
命令應顯示您使用的 ID 專輯的 JSON。如果未找到專輯,您將收到包含錯誤訊息的 JSON。
{ "id": "2", "title": "Jeru", "artist": "Gerry Mulligan", "price": 17.99 }
結論
恭喜!您剛剛使用 Go 和 Gin 編寫了一個簡單的 RESTful Web 服務。
建議的後續主題
- 如果您是 Go 新手,您會在 Effective Go 和 How to write Go code 中找到有用的最佳實踐。
- Go 教程是 Go 基礎知識的絕佳循序漸進介紹。
- 有關 Gin 的更多資訊,請參閱 Gin Web Framework 包文件或 Gin Web Framework 文件。
完成的程式碼
本節包含您透過本教程構建的應用程式的程式碼。
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
// album represents data about a record album.
type album struct {
ID string `json:"id"`
Title string `json:"title"`
Artist string `json:"artist"`
Price float64 `json:"price"`
}
// albums slice to seed record album data.
var albums = []album{
{ID: "1", Title: "Blue Train", Artist: "John Coltrane", Price: 56.99},
{ID: "2", Title: "Jeru", Artist: "Gerry Mulligan", Price: 17.99},
{ID: "3", Title: "Sarah Vaughan and Clifford Brown", Artist: "Sarah Vaughan", Price: 39.99},
}
func main() {
router := gin.Default()
router.GET("/albums", getAlbums)
router.GET("/albums/:id", getAlbumByID)
router.POST("/albums", postAlbums)
router.Run("localhost:8080")
}
// getAlbums responds with the list of all albums as JSON.
func getAlbums(c *gin.Context) {
c.IndentedJSON(http.StatusOK, albums)
}
// postAlbums adds an album from JSON received in the request body.
func postAlbums(c *gin.Context) {
var newAlbum album
// Call BindJSON to bind the received JSON to
// newAlbum.
if err := c.BindJSON(&newAlbum); err != nil {
return
}
// Add the new album to the slice.
albums = append(albums, newAlbum)
c.IndentedJSON(http.StatusCreated, newAlbum)
}
// getAlbumByID locates the album whose ID value matches the id
// parameter sent by the client, then returns that album as a response.
func getAlbumByID(c *gin.Context) {
id := c.Param("id")
// Loop through the list of albums, looking for
// an album whose ID value matches the parameter.
for _, a := range albums {
if a.ID == id {
c.IndentedJSON(http.StatusOK, a)
return
}
}
c.IndentedJSON(http.StatusNotFound, gin.H{"message": "album not found"})
}