教學:使用 Go 和 Gin 開發 RESTful API

本教學介紹使用 Go 和 Gin 網路架構 (Gin) 撰寫 RESTful 網路服務 API 的基礎知識。

如果您對 Go 及其工具具備基本認識,您將能充分利用本教學課程。如果您是第一次接觸 Go,請參閱 教學課程:Go 入門 以快速了解。

Gin 簡化了與建置網路應用程式相關的許多編碼工作,包括網路服務。在本教學課程中,您將使用 Gin 來路由要求、擷取要求詳細資料,以及封送 JSON 以供回應。

在本教學課程中,您將建置一個具有兩個端點的 RESTful API 伺服器。您的範例專案將會是一個關於復古爵士唱片資料的儲存庫。

本教學課程包含以下各節

  1. 設計 API 端點。
  2. 為您的程式碼建立一個資料夾。
  3. 建立資料。
  4. 撰寫一個處理常式來傳回所有項目。
  5. 撰寫一個處理常式來新增一個新項目。
  6. 撰寫一個處理常式來傳回一個特定項目。

注意:有關其他教學課程,請參閱 教學課程

若要嘗試將此作為互動式教學課程在 Google Cloud Shell 中完成,請按一下下列按鈕。

Open in Cloud Shell

先決條件

設計 API 端點

您將建置一個 API,提供存取販售黑膠復古錄音的商店。因此,您需要提供端點,讓客戶端可以為使用者取得和新增專輯。

在開發 API 時,您通常會從設計端點開始。如果端點易於理解,您的 API 使用者將會更成功。

以下是您將在本教學課程中建立的端點。

/albums

/albums/:id

接下來,您將為程式碼建立一個資料夾。

為程式碼建立一個資料夾

首先,為您要撰寫的程式碼建立一個專案。

  1. 開啟命令提示字元,並變更至您的家目錄。

    在 Linux 或 Mac 上

    $ cd
    

    在 Windows 上

    C:\> cd %HOMEPATH%
    
  2. 使用命令提示字元,為您的程式碼建立一個名為 web-service-gin 的目錄。

    $ mkdir web-service-gin
    $ cd web-service-gin
    
  3. 建立一個模組,以便您管理相依性。

    執行 go mod init 命令,並提供您的程式碼將會存在的模組路徑。

    $ go mod init example/web-service-gin
    go: creating new go.mod: module example/web-service-gin
    

    此命令會建立一個 go.mod 檔案,您所新增的相依性將會列在其中以供追蹤。如需進一步瞭解使用模組路徑來命名模組,請參閱 管理相依性

接下來,您將設計資料結構以處理資料。

建立資料

為了讓教學課程更簡單,您將資料儲存在記憶體中。更典型的 API 會與資料庫進行互動。

請注意,將資料儲存在記憶體中表示每當您停止伺服器時,專輯集將會遺失,然後在您啟動伺服器時會重新建立。

撰寫程式碼

  1. 使用您的文字編輯器,在 web-service 目錄中建立一個名為 main.go 的檔案。您將在此檔案中撰寫您的 Go 程式碼。

  2. 在 main.go 中,在檔案的最上方,貼上以下套件宣告。

    package main
    

    一個獨立程式(與函式庫相反)總是位於套件 main 中。

  3. 在套件宣告下方,貼上下列 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"`
    }
    
  4. 在您剛才新增的結構宣告下方,貼上包含您將用來開始的資料的 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 傳回所有專輯。

為此,您將撰寫下列內容

請注意,這與它們在執行時執行的順序相反,但您會先新增相依性,然後再新增依賴它們的程式碼。

撰寫程式碼

  1. 在您在前一節中新增的結構程式碼下方,貼上下列程式碼以取得專輯清單。

    這個 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。實際上,縮排格式在除錯時較容易使用,而且大小差異通常很小。

  2. 在 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 並啟動伺服器。

  3. 在 main.go 的頂端,就在套件宣告下方,匯入您需要支援剛寫的程式碼的套件。

    程式碼的第一行應該看起來像這樣

    package main
    
    import (
        "net/http"
    
        "github.com/gin-gonic/gin"
    )
    
  4. 儲存 main.go。

執行程式碼

  1. 開始追蹤 Gin 模組作為相依性。

    在命令列中,使用 go get 將 github.com/gin-gonic/gin 模組新增為模組的相依性。使用點號引數表示「取得目前目錄中程式碼的相依性」。

    $ go get .
    go get: added github.com/gin-gonic/gin v1.7.2
    

    Go 解析並下載這個相依性,以滿足您在先前的步驟中新增的 import 宣告。

  2. 在包含 main.go 的目錄中,從命令列執行程式碼。使用點號引數表示「執行目前目錄中的程式碼」。

    $ go run .
    

    程式碼執行後,您將有一個正在執行的 HTTP 伺服器,您可以對其傳送要求。

  3. 從新的命令列視窗,使用 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 要求時,您會想要將要求主體中描述的專輯新增到現有的專輯資料。

為此,您將撰寫下列內容

撰寫程式碼

  1. 新增程式碼,將專輯資料新增到專輯清單中。

    貼上以下程式碼,放在 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。
  2. 變更您的 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 方法,分別路由傳送到單一路徑的請求。

執行程式碼

  1. 如果伺服器仍從上一個區段執行中,請停止它。

  2. 從包含 main.go 的目錄中的命令列執行程式碼。

    $ go run .
    
  3. 從另一個命令列視窗,使用 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
    }
    
  4. 如同前一個區段,使用 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 路徑參數相符的專輯。

為執行此項操作,您將

撰寫程式碼

  1. 在您於前一節中新增的 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

  2. 最後,變更您的 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 中,路徑中項目之前的冒號表示該項目為路徑參數。

執行程式碼

  1. 如果伺服器仍從上一個區段執行中,請停止它。

  2. 從包含 main.go 的目錄中的命令列執行程式碼以啟動伺服器。

    $ go run .
    
  3. 從另一個命令列視窗,使用 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 網路服務。

建議的下一個主題

已完成程式碼

此區包含您使用本教學課程建置的應用程式程式碼。

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"})
}