教學:使用 Go 和 Gin 開發 RESTful API
本教學介紹使用 Go 和 Gin 網路架構 (Gin) 撰寫 RESTful 網路服務 API 的基礎知識。
如果您對 Go 及其工具具備基本認識,您將能充分利用本教學課程。如果您是第一次接觸 Go,請參閱 教學課程:Go 入門 以快速了解。
Gin 簡化了與建置網路應用程式相關的許多編碼工作,包括網路服務。在本教學課程中,您將使用 Gin 來路由要求、擷取要求詳細資料,以及封送 JSON 以供回應。
在本教學課程中,您將建置一個具有兩個端點的 RESTful API 伺服器。您的範例專案將會是一個關於復古爵士唱片資料的儲存庫。
本教學課程包含以下各節
- 設計 API 端點。
- 為您的程式碼建立一個資料夾。
- 建立資料。
- 撰寫一個處理常式來傳回所有項目。
- 撰寫一個處理常式來新增一個新項目。
- 撰寫一個處理常式來傳回一個特定項目。
注意:有關其他教學課程,請參閱 教學課程。
若要嘗試將此作為互動式教學課程在 Google Cloud Shell 中完成,請按一下下列按鈕。
先決條件
- 安裝 Go 1.16 或更新版本。有關安裝說明,請參閱 安裝 Go。
- 一個用於編輯程式碼的工具。您擁有的任何文字編輯器都可以正常運作。
- 一個命令終端機。Go 在 Linux 和 Mac 上的任何終端機,以及 Windows 中的 PowerShell 或 cmd 上都能順利運作。
- curl 工具。在 Linux 和 Mac 上,此工具應該已經安裝。在 Windows 上,此工具包含在 Windows 10 Insider 版本 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
中。 -
在套件宣告下方,貼上下列
album
結構的宣告。您將使用它來儲存記憶體中的專輯資料。結構標籤,例如
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) }
在此程式碼中,您
-
撰寫一個
getAlbums
函式,它採用gin.Context
參數。請注意,您可以給這個函式任何名稱,Gin 和 Go 都不需要特定的函式名稱格式。gin.Context
是 Gin 中最重要的部分。它會傳遞請求詳細資料、驗證並序列化 JSON 等。(儘管名稱類似,這與 Go 內建的context
套件不同。) -
呼叫
Context.IndentedJSON
將結構序列化為 JSON 並將其新增至回應。此函式的第一個引數是要傳送給用戶端端的 HTTP 狀態碼。在此,您傳遞
StatusOK
常數(來自net/http
套件)以表示200 OK
。請注意,您可以用呼叫
Context.JSON
來取代Context.IndentedJSON
,以傳送更精簡的 JSON。實際上,縮排格式在除錯時較容易使用,而且大小差異通常很小。
-
-
在 main.go 的頂端附近,就在
albums
切片宣告的下方,貼上以下程式碼,將處理函式指定給端點路徑。這會設定一個關聯,其中
getAlbums
會處理對/albums
端點路徑的請求。func main() { router := gin.Default() router.GET("/albums", getAlbums) router.Run("localhost:8080") }
在此程式碼中,您
-
在 main.go 的頂端,就在套件宣告下方,匯入您需要支援剛寫的程式碼的套件。
程式碼的第一行應該看起來像這樣
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
對正在執行的網路服務提出要求。$ curl https://127.0.0.1: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
要求以新增項目的程式碼。
撰寫處理程式以新增新項目
當客戶端在 /albums
提出 POST
要求時,您會想要將要求主體中描述的專輯新增到現有的專輯資料。
為此,您將撰寫下列內容
- 將新專輯新增到現有清單的邏輯。
- 一段程式碼,將
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") }
在此程式碼中,您
-
將
/albums
路徑的POST
方法與postAlbums
函式關聯起來。使用 Gin,您可以將處理常式與 HTTP 方法和路徑組合關聯起來。這樣一來,您可以根據客戶端使用的 HTTP 方法,分別路由傳送到單一路徑的請求。
-
執行程式碼
-
如果伺服器仍從上一個區段執行中,請停止它。
-
從包含 main.go 的目錄中的命令列執行程式碼。
$ go run .
-
從另一個命令列視窗,使用
curl
對您執行的網路服務提出請求。$ curl https://127.0.0.1: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://127.0.0.1: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,並以 HTTP 程式碼200 OK
作為回應傳回。如上所述,實際的服務可能會使用資料庫查詢來執行此查詢。
-
如果找不到專輯,請傳回 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
對您執行的網路服務提出請求。$ curl https://127.0.0.1:8080/albums/2
此指令應顯示您所使用 ID 的專輯的 JSON。如果找不到專輯,您會收到包含錯誤訊息的 JSON。
{ "id": "2", "title": "Jeru", "artist": "Gerry Mulligan", "price": 17.99 }
結論
恭喜!您剛剛使用 Go 和 Gin 寫了一個簡單的 RESTful 網路服務。
建議的下一個主題
- 如果您是 Go 的新手,您會發現 Effective Go 和 如何撰寫 Go 程式碼 中描述的有用的最佳實務。
- Go Tour 是逐步了解 Go 基礎知識的絕佳入門。
- 若要深入了解 Gin,請參閱 Gin 網路架構套件文件 或 Gin 網路架構文件。
已完成程式碼
此區包含您使用本教學課程建置的應用程式程式碼。
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"})
}