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
寫法偏舊,未來有空會再寫一個新的系列,有興趣再往下閱讀
建立專案資料夾與檔案
現在先長這樣,資料夾 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, ¶ms) // 使用 `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, ¶ms)
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