iT邦幫忙

2023 iThome 鐵人賽

DAY 19
0
自我挑戰組

Techschool Goalng Backend Master Class 的學習記錄系列 第 19

[Day 19] Implement RESTful in GO using Gin Part 2

  • 分享至 

  • xImage
  •  

Implement create account API

  • server.go 的NewServer中加入 createAccountrouter

    server.go
    
    func NewServer(store *db.Store) *Server {
    	server := &Server{store: store}
    	router := gin.Default()
    
    	// TODO: add routes
    	router.POST("/accounts", server.createAccount)
    
    	server.router = router
    	return server
    }
    
  • api 資料夾內的新檔案 account.go 中實作 server.createAccount API,其func namecreateAccount,並將 gin.Context 物件作為input

  • gin.Context 有一個 .Request 字段,它的類型是 **http.Request**專為 Gin 的 HTTP 請求/響應處理而設計。

  • 參考account.sql.go 宣告過CreateAccountParams來設定createAccountRequest,其中Account 初始的Balance 為0,所以我們可以移除Balance欄位。

  • 驗證輸入數據: 每當我們從客戶端獲取輸入數據時,最好進行驗證。為此,這裡使用*go validator*綁定tag到Owner和Currency欄位。目前我們的銀行只支援三種貨幣:USD 、 EUR和CAD (使用oneof進行驗證)。

  • errorResponse() 函數,將錯誤(error)轉換成一個 gin.H (key-value object), 這樣可以輕鬆地將其序列化為JSON並返回給Client。

    server.go
    func errorResponse(err error) gin.H {
        return gin.H{"error": err.Error()}
    }
    
  • createAccount 函數的實作: 如果Client提供的數據無效,我們應該向Client發送400 Bad Request響應,而無法在Database建立Account時則向Client發送500 Internal Server Error ,反正成功建立為200 OK

package api

import (
	"net/http"

	db "github.com/Kcih4518/simpleBank_2023/db/sqlc"
	"github.com/gin-gonic/gin"
)

// Server serves HTTP requests for our banking service.
// Using go validator to validate the request body
type createAccountRequest struct {
	Owner    string `json:"owner" binding:"required"`
	Currency string `json:"currency" binding:"required,oneof=USD EUR CAD"`
}

// errorResponse is a helper to format error message
// Using ShouldBindJSON to bind the request body to the request struct
func (server *Server) createAccount(ctx *gin.Context) {
	var req createAccountRequest
	if err := ctx.ShouldBindJSON(&req); err != nil {
		ctx.JSON(http.StatusBadRequest, errorResponse(err))
		return
	}
	arg := db.CreateAccountParams{
		Owner:    req.Owner,
		Balance:  0,
		Currency: req.Currency,
	}
	account, err := server.store.CreateAccount(ctx, arg)

	if err != nil {
		ctx.JSON(http.StatusInternalServerError, errorResponse(err))
		return
	}
	ctx.JSON(http.StatusOK, account)
}

Q & A:

  1. ctx *gin.Contextctx context.Context是什麼? 有和不同呢? 又該怎麼選擇呢?

    1. ctx context.Context:

      • context.Context 是 Go 標準庫的一個型別,用於在API邊界和進程之間傳遞截止日期、取消信號和其他請求範疇的值。它在 Go 中被普遍用於跨函數邊界傳遞上下文信息,尤其是用於超時和取消信號。
      • 它被設計用於在請求鏈或不同的 goroutines 之間傳遞上下文和發送取消信號。
      • 它常用於資料庫操作、網絡請求或可能需要被取消或超時的任何操作。
    2. ctx gin.Context:

      • gin.Context 是 Gin web 框架提供的特定型別。它代表一個特定的 HTTP 請求的上下文,並提供與請求和響應互動的方法,例如讀取請求參數、設置響應頭等。
      • 它確實有自己的一套值(類似於 context.Context),但它更專為 Gin 框架的 HTTP 請求/響應處理而設計。
      • 除了請求/響應操作外,gin.Context 還帶有關於當前路由、中間件等的信息。
    3. 差異性:
      https://ithelp.ithome.com.tw/upload/images/20231004/20121746PrvDtNUqY9.png

      • 雖然兩者都提供了一種攜帶值和狀態的方式,但 context.Context 更為通用,用於在各種類型的操作中攜帶上下文,不僅僅是 HTTP 請求。
      • gin.Context 有一個 .Request 字段,它的類型是 **http.Request**專為 Gin 的 HTTP 請求/響應處理而設計。
    4. 使用情境:

      • 如果您在 Gin 應用中的一個 HTTP 處理程序內,並需要與當前的 HTTP 請求/響應互動,請使用 gin.Context
      • 如果您正在寫一個可能在多個上下文中使用的函數(不僅僅是 HTTP 處理程序),並可能受益於超時、取消或攜帶其他上下文值,請使用 context.Context

    Start HTTP server

    • Server 結構體中添加一個新的 Start() 函數。此函數將輸入一個地址並回傳一個錯誤,其角色是在輸入地址上運行 HTTP 伺服器以開始監聽 API 請求。

    • Gin 已經在router中提供了一個函數來執行此操作,所以我們需要做的只是調用 server.router.Run(),並傳入address

    • 在 Go 中,如果一個字段或函數的名稱以小寫字母開始,那麼它是Private,只能在其所屬的package中被存取。如果它以大寫字母開始,則它是Public,可以被任何其他package存取。

    • 因此**server.router** 字段是Private,所以它不能從這個 api 套件的外部被訪問。

      // Start HTTP server
      func (server *Server) Start(address string) error {
      	return server.router.Run(address)
      }
      

    Entry point for server

    • 接下來在根目錄建立 main.go ,做為我們Backend ServerEntry Point

    • Package name: main,並且它應該有一個 main() func。

    • 並且Backend Server 也需要於Database進行連線,於之前在db/sqlc/main_test.go 所實現的方式相似。

    • 其中使用 db.NewStore() 函數創建一個New Store

    • 然後,我們通過調用 api.NewServer() 來創建一個New Server,並傳入該Store

    • 並透過server.Start() 代入serverAddress ,來建立一個Http Server : http://localhost:8080

    • 另外需blank import lib/pq ,這樣才有辦法與PostgreSQL 進行溝通。

      ~/main.go
      
      package main
      
      import (
      	"database/sql"
      	"log"
      
      	"github.com/Kcih4518/simpleBank_2023/api"
      	db "github.com/Kcih4518/simpleBank_2023/db/sqlc"
      	_ "github.com/lib/pq"
      )
      
      const (
      	dbDriver      = "postgres"
      	dbSource      = "postgresql://root:secret@localhost:5432/simple_bank?sslmode=disable"
      	serverAddress = "0.0.0.0:8080"
      )
      
      // Entry point for the application
      func main() {
      	conn, err := sql.Open(dbDriver, dbSource)
      	if err != nil {
      		log.Fatal("cannot connect to db: ", err)
      	}
      
      	store := db.NewStore(conn)
      	server := api.NewServer(store)
      	err = server.Start(serverAddress)
      	if err != nil {
      		log.Fatal("cannot start server:", err)
      	}
      }
      
    • 最後再Makefile 新增一個run server command:

      Makefile
      ...
      
      server:
          go run main.go
      
      .PHONY: postgres createdb dropdb migrateup migratedown sqlc test
      
    • 透過指令建立http server

      $ make server
      go run main.go
      [GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
      
      [GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
       - using env:	export GIN_MODE=release
       - using code:	gin.SetMode(gin.ReleaseMode)
      
      [GIN-debug] POST   /accounts                 --> github.com/Kcih4518/simpleBank_2023/api.(*Server).createAccount-fm (3 handlers)
      [GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.
      Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.
      [GIN-debug] Listening and serving HTTP on 0.0.0.0:8080
      

Code Flow

  1. main.go 中,程序首先嘗試連接到數據庫。
  2. 如果連接成功,它將使用 db.NewStore 創建一個新的存儲。
  3. 接著,它會使用 api.NewServer 函數(定義在 server.go 中)來創建一個新的伺服器實例。
  4. server.go 中,NewServer 函數初始化 Server 結構體,設置預設的 Gin 路由器,添加路由,然後將路由器分配給 server.router
  5. 最後,回到 main.go 中,程序嘗試啟動伺服器。如果啟動過程中出現錯誤,它會終止程序。

Test create account API with Postman


  • C**reate Account Request**

    • Method:POST
    • URL: http://localhost:8080/accounts
    • JSON Body:
      • 選擇Body標籤,選擇Raw,並選擇JSON格式。

      • 添加2個輸入字段:擁有者的名字(這裡使用我的名字)和貨幣(例如USD)。

        {
            "owner": "Quang Pham",
            "currency": "USD"
        }
        
        
      • 點擊Send

  • 成功回應:

    • 我們收到200 OK狀態碼,並獲得創建的帳戶物件。
    • 該帳戶具有ID=1、餘額=0,並且擁有正確的擁有者名稱和貨幣。

    https://ithelp.ithome.com.tw/upload/images/20231004/20121746OA4RiDOg1H.png

  • 測試無效數據:

    • 設置兩個字段為空字符串,然後點擊Send

      {
          "owner": "",
          "currency": ""
      }
      
      
    • 這次,我們收到了400 Bad Request,並收到一條說明字段是必需的錯誤消息。這個錯誤消息看起來很難讀,因為它將2個字段的驗證錯誤結合在一起。這是我們未來可能想改進的地方。

      https://ithelp.ithome.com.tw/upload/images/20231004/201217469Zfy80zquj.png

  • 嘗試使用無效的貨幣代碼,例如xyz。

    {
        "owner": "Quang Pham",
        "currency": "xyz"
    }
    
    
    • 這次,我們也收到400 Bad Request狀態碼,但錯誤消息不同。它表示在oneof標籤上的驗證失敗,這正是我們想要的,因為在代碼中我們只允許貨幣的2個可能值:USD和EUR。

      https://ithelp.ithome.com.tw/upload/images/20231004/201217466qM19JH7rm.png


上一篇
[Day 18] Implement RESTful in GO using Gin Part 1
下一篇
[Day 20] Implement RESTful in GO using Gin Part 3
系列文
Techschool Goalng Backend Master Class 的學習記錄31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言