• English
  • 使用 Go 和 Gin 開發 RESTful API

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

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

    Gin 簡化了許多與建構 Web 應用相關的編碼任務,包括 Web 服務。在本教學中,你將使用 Gin 來路由請求、檢索請求細節並編組回應的 JSON。

    在本教學中,你將建構一個具有兩個端點的 RESTful API 伺服器。你的範例項目將是一個關於復古爵士唱片的資料儲存庫。

    本教學包括以下部分:

    1. 設計 API 端點。
    2. 為程式碼建立一個資料夾。
    3. 建立資料。
    4. 撰寫處理程式以返回所有項目。
    5. 撰寫處理程式以加入新項目。
    6. 撰寫處理程式以返回特定項目。

    要將其作為在 Google Cloud Shell 中完成的互動式教學,請點擊下面的按鈕。

    先決條件

    • Go 1.16 或更高版本的安裝。 有關安裝說明,請參閱 安裝 Go
    • 一個用於編輯程式碼的工具。 任何你擁有的文字編輯器都可以正常工作。
    • 一個命令終端。 Go 在 Linux 和 Mac 上的任何終端上都能很好地工作,在 Windows 上的 PowerShell 或 cmd 上也是如此。
    • curl 工具。 在 Linux 和 Mac 上,這應該已經安裝。在 Windows 上,它包含在 Windows 10 Insider build 17063 及更高版本中。對於較早的 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. 在套件宣告下方,貼上以下 album 結構宣告。你將使用此結構在記憶體中儲存專輯資料。

      結構標籤如 json:"artist" 指定了當結構內容序列化為 JSON 時欄位的名稱。沒有它們,JSON 將使用結構體的大寫欄位名 —— 這在 JSON 中不常見。

      // album 表示唱片專輯資訊。
      type album struct {
      	ID     string  `json:"id"`
      	Title  string  `json:"title"`
      	Artist string  `json:"artist"`
      	Price  float64 `json:"price"`
      }
    4. 在你剛剛加入的結構宣告下方,貼上以下包含專輯資料的結構切片。

      // albums 切片用於儲存專輯資料。
      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 以 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.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()(注意括號)來實現。

      • 使用 Run 函數將路由器附加到 http.Server 並啟動伺服器。

    3. main.go 頂部附近,緊接在套件宣告下方,匯入你剛撰寫程式碼所需的套件。

      程式碼的前幾行應如下所示:

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

    執行程式碼

    1. 開始將 Gin 模組作為依賴項進行追蹤。

      在命令列中,使用 go getgithub.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 http://localhost: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 請求路由到你的邏輯的一小段程式碼

    撰寫程式碼

    1. 加入程式碼以將專輯資料加入到專輯列表中。

      import 敘述之後的某個地方,貼上以下程式碼。(檔案的結尾是這段程式碼的好位置,但 Go 不強制你宣告函數的順序。)

      // postAlbums 從請求本文中接收的 JSON 加入專輯。
      func postAlbums(c *gin.Context) {
      	var newAlbum album
      
      	// 呼叫 BindJSON 將接收到的 JSON 綁定到
      	// newAlbum。
      	if err := c.BindJSON(&newAlbum); err != nil {
      		return
      	}
      
      	// 將初始化的專輯結構體附加到切片中。
      	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 http://localhost: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 http://localhost: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 定位 ID 值與客戶端傳送的 id
      // 參數匹配的專輯,然後返回該專輯作為回應。
      func getAlbumByID(c *gin.Context) {
      	id := c.Param("id")
      
      	// 遍歷專輯列表,尋找
      	// 其 ID 欄位值與參數值匹配的專輯。
      	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.StatusNotFound 返回 HTTP 404 錯誤。

    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 http://localhost: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 結構體用來表示唱片專輯資料
    type album struct {
    	ID     string  `json:"id"`
    	Title  string  `json:"title"`
    	Artist string  `json:"artist"`
    	Price  float64 `json:"price"`
    }
    
    // albums 切片用來儲存專輯資料
    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 函式以 JSON 格式回傳所有專輯列表
    func getAlbums(c *gin.Context) {
    	c.IndentedJSON(http.StatusOK, albums)
    }
    
    // postAlbums 函式從請求中讀取 JSON 並新增專輯
    func postAlbums(c *gin.Context) {
    	var newAlbum album
    
    	// 使用 BindJSON 將收到的 JSON 資料綁定到 newAlbum 變數
    	if err := c.BindJSON(&newAlbum); err != nil {
    		return
    	}
    
    	// 將新專輯新增至切片
    	albums = append(albums, newAlbum)
    	c.IndentedJSON(http.StatusCreated, newAlbum)
    }
    
    // getAlbumByID 函式根據客戶端傳入的 ID 參數查找對應專輯
    func getAlbumByID(c *gin.Context) {
    	id := c.Param("id")
    
    	// 遍歷專輯列表,查找 ID 與參數相符的專輯
    	for _, a := range albums {
    		if a.ID == id {
    			c.IndentedJSON(http.StatusOK, a)
    			return
    		}
    	}
    	c.IndentedJSON(http.StatusNotFound, gin.H{"message": "album not found"})
    }