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

本教程將介紹使用 Go 和 Gin Web Framework (Gin) 編寫 RESTful Web 服務 API 的基礎知識。

如果您對 Go 及其工具鏈有基本的瞭解,將能從本教程中獲得最大收益。如果您是第一次接觸 Go,請參閱 教程:Go 入門 以快速瞭解。

Gin 簡化了構建 Web 應用程式(包括 Web 服務)的許多編碼任務。在本教程中,您將使用 Gin 來路由請求、檢索請求詳細資訊以及為響應進行 JSON 編組。

在本教程中,您將構建一個具有兩個端點的 RESTful API 伺服器。您的示例專案將是一個關於老式爵士唱片資料的儲存庫。

本教程包含以下章節

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

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

要以互動式教程的形式完成此操作,您可以在 Google Cloud Shell 中完成,請點選下方按鈕。

Open in 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 格式返回專輯資料。

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

為您的程式碼建立一個資料夾

首先,建立一個專案來存放您將編寫的程式碼。

  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. 在 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"`
    }
    
  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)
    }
    

    在此程式碼中,您

    • 編寫一個接受 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。實際上,縮排形式在除錯時更容易使用,並且尺寸差異通常很小。

  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 宣告下方,匯入您將需要的包來支援您剛剛編寫的程式碼。

    程式碼的第一行應如下所示:

    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 向您正在執行的 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 請求路由到您的邏輯。

編寫程式碼

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

    在此程式碼中,您

    • POST 方法在 /albums 路徑與 postAlbums 函式關聯起來。

      使用 Gin,您可以將處理器與 HTTP 方法和路徑的組合關聯起來。透過這種方式,您可以根據客戶端使用的方法,根據傳送到單個路徑的請求分別進行路由。

執行程式碼

  1. 如果伺服器在上一個部分仍然執行,請將其停止。

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

    $ go run .
    
  3. 從另一個命令列視窗,使用 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
    }
    
  4. 與上一節一樣,使用 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 路徑引數匹配的專輯。

為此,您將

  • 新增檢索所請求專輯的邏輯。
  • 將路徑對映到邏輯。

編寫程式碼

  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,並以 200 OK HTTP 程式碼作為響應返回。

      如上所述,真實世界的服務可能使用資料庫查詢來執行此查詢。

    • 如果未找到專輯,則返回 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 向您正在執行的 Web 服務發出請求。

    $ curl https://:8080/albums/2
    

    命令應顯示您使用的 ID 專輯的 JSON。如果未找到專輯,您將收到包含錯誤訊息的 JSON。

    {
            "id": "2",
            "title": "Jeru",
            "artist": "Gerry Mulligan",
            "price": 17.99
    }
    

結論

恭喜!您剛剛使用 Go 和 Gin 編寫了一個簡單的 RESTful Web 服務。

建議的後續主題

完成的程式碼

本節包含您透過本教程構建的應用程式的程式碼。

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