Go Gin 建立 API - 建立與整理專案

tags: Go Gin API
category: Back-End
description: Go Gin 建立 API - 建立與整理專案,抽離 router, request, response, errors
created_at: 2021/07/09 10:00:00

cover image


回到 手把手開始 Go Gin 建立 API


寫法偏舊,未來有空會再寫一個新的系列,有興趣再往下閱讀


建立專案資料夾與檔案

現在先長這樣,資料夾 gin-api , 主程式 main.go

<gin-api>
  main.go

再來建立 go module , 把他想像是你寫 node.js 的 package.json 或 python 的 env 吧 (?)

$ go mod init <url>

<url> 你可以去你的 github 開一個 repository 然後就貼那段網址,或是如果你不知道要填什麼的話就填 example.com

假設我就填這樣,所以你可以在這裡找到這個專案

$ go mod init github.com/LaiJunBin/gin-api

之後你的專案下會長出一個 go.mod 就把他想像是 package.json 吧

再來安裝 gin 這個套件

$ go get -u github.com/gin-gonic/gin

之後在你的 main.go 寫上

package main

import (
    "github.com/gin-gonic/gin"
    "net/http"
)

func main() {
    router := gin.New()

    router.GET("/", func(c *gin.Context) {
        c.JSON(200, gin.H{})
    })

    router.GET("/test", func(c *gin.Context) {
        c.JSON(200, gin.H{
            "success": true,
            "data": "Hello world.",
        })
    })

    s := &http.Server{
        Addr:           ":8080",
        Handler:        router,
    }

    s.ListenAndServe()
}

這個是最基本的範例 (監聽 8080 Port)

用下面的指令執行 Server

$ go run main.go

/ 會得到 {}

/test 會得到

{
    "data": "Hello world.",
    "success": true
}

稍微解釋一下 gin.H 這個是什麼玩意,他其實很單純,只是一個別名,簡短的寫法,因為我第一次看到也很好奇

type H map[string]interface{}

抽離 Router

認真整理一下檔案,從 router 開始,所以現在變成這樣

<gin-api>
  <internal>
    <routers>
      router.go
  main.go

internal 用來放內部程式的檔案,然後把程式改成這樣

router.go

package routers

import "github.com/gin-gonic/gin"

func NewRouter() *gin.Engine {
    router := gin.New()

    router.GET("/", func(c *gin.Context) {
        c.JSON(200, gin.H{})
    })

    router.GET("/test", func(c *gin.Context) {
        c.JSON(200, gin.H{
            "success": true,
            "data": "Hello world.",
        })
    })

    return router
}

main.go

package main

import (
    "github.com/LaiJunBin/gin-api/internal/routers"
    "net/http"
)

func main() {
    router := routers.NewRouter()

    s := &http.Server {
        Addr:           ":8080",
        Handler:        router,
    }

    s.ListenAndServe()
}

這樣就無痛抽離 router 了,那再來把 router 中的邏輯做抽離,再建立一個檔案

<gin-api>
  <internal>
    <routers>
      <api>
        hello.go <-- 建立這個
      router.go
  main.go

hello.go

package api

import "github.com/gin-gonic/gin"

type Hello struct {}

func NewHello() Hello {
    return Hello{}
}

func (h Hello) Index(c *gin.Context) {
    c.JSON(200, gin.H{})
}

func (h Hello) Test(c *gin.Context) {
    c.JSON(200, gin.H{
        "success": true,
        "data": "Hello world.",
    })
}

router.go

package routers

import (
    "github.com/LaiJunBin/gin-api/internal/routers/api"
    "github.com/gin-gonic/gin"
)

func NewRouter() *gin.Engine {
    router := gin.New()

    hello := api.NewHello()
    router.GET("/", hello.Index)
    router.GET("/test", hello.Test)

    return router
}

這樣無痛的把邏輯又抽出來了

再來寫寫看路由參數與 POST 資料

router.go

// ...
router.POST("/form", hello.PostForm)
router.POST("/json", hello.PostJson)
router.GET("/:id", hello.Get)
// ...

hello.go

// ...
func (h Hello) Get(c *gin.Context) {
    id := c.Param("id") // 取得路由參數
    c.JSON(200, gin.H{
        "id": id,
    })
}

func (h Hello) PostForm(c *gin.Context) {
    data := c.PostForm("data") // 取得表單資料

    c.JSON(200, gin.H{
        "success": true,
        "data": data,
    })
}


func (h Hello) PostJson(c *gin.Context) {
    jsonData := make(gin.H)
    err := c.BindJSON(&jsonData) // 取得json資料
    if err != nil {
        // error handle.
    }

    c.JSON(200, gin.H{
        "success": true,
        "data": jsonData["data"],
    })
}

抽離 Request

這樣子做很麻煩,想像一下如果還要驗證資料,所以我們再抽出一層 requests,然後順便該是時候認真一些了,把 hello.go 刪除, 建立一個 user.go 當作範例,所以這時檔案結構變成

<gin-api>
  <internal>
    <requests>
      user.go  <-- 多了這個
    <routers>
      <api>
        user.go <-- 改了這個
    <pkg>
      <app>
        request.go <-- 多了這個
      router.go
  main.go

路由先做最基本的註冊與登入

user := api.NewUser()
router.POST("/register", user.Register)
router.POST("/login", user.Login)

requests/user.go

package requests

type RegisterUserRequest struct {
    Name string `form:"name" binding:"required,max=100"`
    Username string `form:"username" binding:"required,max=100"`
    Password string `form:"password" binding:"required,max=100"`
}

type LoginUserRequest struct {
    Username string `form:"username" binding:"required,max=100"`
    Password string `form:"password" binding:"required,max=100"`
}

寫出需要哪些欄位跟驗證方式

再來寫一下 request.go

package app

import (
    "github.com/gin-gonic/gin"
    "github.com/go-playground/validator/v10"
)

func BindAndValidation(c *gin.Context, v interface{}) (bool, []string) {
    var errors []string

    if err := c.ShouldBind(v); err != nil {
        for _, e := range err.(validator.ValidationErrors) {
            errors = append(errors, e.Error())
        }
        return false, errors
    }

    return true, nil
}

BindAndValidation() 可以供之後定義的 request 做使用

api/user.go

package api

import (
    "github.com/LaiJunBin/gin-api/internal/requests"
    "github.com/LaiJunBin/gin-api/pkg/app"
    "github.com/gin-gonic/gin"
)

type User struct {}

func NewUser() User {
    return User{}
}

func (u User) Register(c *gin.Context) {
    params := requests.RegisterUserRequest{}
    valid, err := app.BindAndValidation(c, &params) // 使用 `request.go` 驗證並取值進 `params`

    if !valid { // 驗證沒過
        c.JSON(400, gin.H{
            "success": false,
            "message": err,
        })
        return
    }

    c.JSON(200, gin.H{
        "success": true,
        "data": params, // 看看我拿到什麼
    })
}

func (u User) Login(c *gin.Context) {
    // ...
}

抽離 Response, Error

再來感覺 c.JSON 回應那一段重複有點多次,也把他抽出來吧

但再抽出他之前,先把錯誤訊息也處理一下,所以檔案目錄變成

<gin-api>
  <internal>
    <requests>
      user.go
    <routers>
      <api>
        user.go
    <pkg>
      <app>
        request.go
        response.go <-- 處理回應
      <errors>
        error.go    <-- Error 結構
        errors.go   <-- 定義一些錯誤訊息
        http.go     <-- 定義一些http出錯時的status code & message
      router.go
  main.go

error.go

package errors

import "fmt"

type Error struct {
    Code    int  `json:"cond"`
    Message string   `json:"message"`
    Details []string `json:"details"`
}

func NewError(code int, message string) *Error {
    return &Error{Code: code, Message: message}
}

func (e *Error) GetError() string {
    return fmt.Sprintf("Error: %d, message: %s", e.GetCode(), e.GetMessage())
}

func (e *Error) GetCode() int {
    return e.Code
}

func (e *Error) GetMessage() string {
    return e.Message
}

func (e *Error) GetDetails() []string {
    return e.Details
}

func (e *Error) WithDetails(details ...string) *Error {
    newError := *e
    newError.Details = append(newError.Details, details...)
    return &newError
}

errors.go

package errors

var (
    Success                   = NewError(200, "Success")
    ServerError               = NewError(500, "ServerError")
    InvalidParams             = NewError(400, "InvalidParams")
    NotFound                  = NewError(404, "NotFound")
    UnauthorizedAuthNotExist  = NewError(401, "UnauthorizedAuthNotExist")
    UnauthorizedTokenError    = NewError(401, "UnauthorizedTokenError")
    UnauthorizedTokenTimeout  = NewError(401, "UnauthorizedTokenTimeout")
    UnauthorizedTokenGenerate = NewError(401, "UnauthorizedTokenGenerate")
    TooManyRequests           = NewError(429, "TooManyRequests")
)

http.go

package errors

var (
    StatusBadRequest                   = NewError(400, "StatusBadRequest")
    StatusUnauthorized                 = NewError(401, "StatusUnauthorized")
    StatusPaymentRequired              = NewError(402, "StatusPaymentRequired")
    StatusForbidden                    = NewError(403, "StatusForbidden")
    StatusNotFound                     = NewError(404, "StatusNotFound")
    StatusMethodNotAllowed             = NewError(405, "StatusMethodNotAllowed")
    StatusNotAcceptable                = NewError(406, "StatusNotAcceptable")
    StatusProxyAuthRequired            = NewError(407, "StatusProxyAuthRequired")
    StatusRequestTimeout               = NewError(408, "StatusRequestTimeout")
    StatusConflict                     = NewError(409, "StatusConflict")
    StatusGone                         = NewError(410, "StatusGone")
    StatusLengthRequired               = NewError(411, "StatusLengthRequired")
    StatusPreconditionFailed           = NewError(412, "StatusPreconditionFailed")
    StatusRequestEntityTooLarge        = NewError(413, "StatusRequestEntityTooLarge")
    StatusRequestURITooLong            = NewError(414, "StatusRequestURITooLong")
    StatusUnsupportedMediaType         = NewError(415, "StatusUnsupportedMediaType")
    StatusRequestedRangeNotSatisfiable = NewError(416, "StatusRequestedRangeNotSatisfiable")
    StatusExpectationFailed            = NewError(417, "StatusExpectationFailed")
    StatusTeapot                       = NewError(418, "StatusTeapot")
    StatusMisdirectedRequest           = NewError(421, "StatusMisdirectedRequest")
    StatusUnprocessableEntity          = NewError(422, "StatusUnprocessableEntity")
    StatusLocked                       = NewError(423, "StatusLocked")
    StatusFailedDependency             = NewError(424, "StatusFailedDependency")
    StatusTooEarly                     = NewError(425, "StatusTooEarly")
    StatusUpgradeRequired              = NewError(426, "StatusUpgradeRequired")
    StatusPreconditionRequired         = NewError(428, "StatusPreconditionRequired")
    StatusTooManyRequests              = NewError(429, "StatusTooManyRequests")
    StatusRequestHeaderFieldsTooLarge  = NewError(431, "StatusRequestHeaderFieldsTooLarge")
    StatusUnavailableForLegalReasons   = NewError(451, "StatusUnavailableForLegalReasons")

    StatusInternalServerError           = NewError(500, "StatusInternalServerError")
    StatusNotImplemented                = NewError(501, "StatusNotImplemented")
    StatusBadGateway                    = NewError(502, "StatusBadGateway")
    StatusServiceUnavailable            = NewError(503, "StatusServiceUnavailable")
    StatusGatewayTimeout                = NewError(504, "StatusGatewayTimeout")
    StatusHTTPVersionNotSupported       = NewError(505, "StatusHTTPVersionNotSupported")
    StatusVariantAlsoNegotiates         = NewError(506, "StatusVariantAlsoNegotiates")
    StatusInsufficientStorage           = NewError(507, "StatusInsufficientStorage")
    StatusLoopDetected                  = NewError(508, "StatusLoopDetected")
    StatusNotExtended                   = NewError(510, "StatusNotExtended")
    StatusNetworkAuthenticationRequired = NewError(511, "StatusNetworkAuthenticationRequired")
)

response.go

package app

import (
    "github.com/LaiJunBin/gin-api/pkg/errors"
    "github.com/gin-gonic/gin"
    "net/http"
)

type Response struct {
    Ctx *gin.Context
}

func NewResponse(ctx *gin.Context) *Response {
    return &Response{Ctx: ctx}
}

func (r *Response) MakeResponse(data interface{}) {
    if data == nil {
        data = gin.H{}
    }

    r.Ctx.JSON(http.StatusOK, gin.H{
        "success": true,
        "data":    data,
    })
}

func (r *Response) MakeErrorResponse(error *errors.Error) {
    newResponse := gin.H{
        "success": false,
        "message": error.GetMessage(),
        "details": error.GetDetails(),
    }

    r.Ctx.JSON(error.GetCode(), newResponse)
}

再來就可以這麼使用 api/user.go

func (u User) Register(c *gin.Context) {
    params := requests.RegisterUserRequest{}
    response := app.NewResponse(c)
    valid, err := app.BindAndValidation(c, &params)

    if !valid {
        errorResponse := errors.InvalidParams.WithDetails(err...)
        response.MakeErrorResponse(errorResponse)
        return
    }

    response.MakeResponse(params)
}

這時候驗證沒過,就會顯示像是下面這樣的訊息

{
    "details": [
        "Key: 'RegisterUserRequest.Name' Error:Field validation for 'Name' failed on the 'required' tag"
    ],
    "message": "InvalidParams",
    "success": false
}

如果驗證過,有正常回應 data

{
    "data": {
        "Name": "Hello",
        "Username": "user01",
        "Password": "1234"
    },
    "success": true
}

最終的檔案目錄結構

<gin-api>
  <internal>
    <requests>
      user.go
    <routers>
      <api>
        user.go
    <pkg>
      <app>
        request.go
        response.go
      <errors>
        error.go
        errors.go
        http.go
      router.go
  main.go

這篇已經有點長,下一篇來寫多國語言(i18n)、中介層(middleware)、設定檔與資料庫(database)

這篇進度的Source code




最後更新時間: 2021年07月09日.