基於go語言gin框架的web專案骨架

青絲南開發表於2023-09-24

節省時間與精力,更高效地打造穩定可靠的Web專案:基於Go語言和Gin框架的完善Web專案骨架。無需從零開始,直接利用這個骨架,快速搭建一個功能齊全、效能優異的Web應用。充分發揮Go語言和Gin框架的優勢,輕鬆處理高併發、大流量的請求。構建可擴充套件性強、易於維護的程式碼架構,保證專案的長期穩定執行。同時,透過整合常用功能模組和最佳實踐,減少繁瑣的開發工作,使您專注於業務邏輯的實現。

該骨架每個元件之間可單獨使用,元件之間松耦合,高內聚,元件的實現基於其他三方依賴包的封裝。
目前該骨架實現了大多數的元件,比如事件,中介軟體,日誌,配置,引數驗證,命令列,定時任務等功能,目前可以滿足大多數開發需求,後續會持續維護更新功能。

github地址:https://github.com/czx-lab/skeleton

設定環境變數並下載專案依賴

go env -w GO111MODULE=on
go env -w GOPROXY=https://goproxy.cn,direct
go mod download

執行專案

go run ./cmd/main.go

專案編譯打包執行

go build ./cmd/main.go

// 編譯
make build

// 執行
make run

// 編譯與執行
make

// 執行專案
./main

專案目錄結構說明

├─app
│  ├─command ---> 命令列
│  ├─controller
│  │    └─base.go ---> BaseController,主要定義了request引數驗證器validator
│  ├─event
│  │  ├─entity ---> 事件實體目錄
│  │  ├─listen ---> 事件監聽執行指令碼目錄
│  │  └─event.go ---> 事件註冊程式碼
│  │       
│  ├─middleware ---> 中介軟體程式碼目錄
│  ├─request ---> 請求引數校驗程式碼目錄
│  │   └─request.go ---> 引數驗證器
│  └─task ---> 定時任務程式碼目錄
│     └─task.go ---> 註冊定時任務指令碼
├─cmd ---> 專案入口目錄
│  └─cli ---> 專案命令列模式入口目錄
├─config
│  └─config.yaml ---> 配置檔案
├─internal ---> 包含第三方包的封裝
├─router ---> 路由目錄
│  └─router.go
├─storage ---> 日誌、資源儲存目錄
│  └─logs
└─test ---> 單元測試目錄

基礎功能


路由

該骨架的web框架是gin,所以路由定義可直接閱讀Gin框架的文件。

在該骨架中定義註冊路由需要在router資料夾下面的router.go檔案中的func (*AppRouter) Add(server *gin.Engine)方法定義註冊:

server.GET("/foo", func(ctx *gin.Context) {
    ctx.String(http.StatusOK, "hello word!")
})

也可以透過自己定義路由的定義註冊,只需要實現github.com/czx-lab/skeleton/internal/server/router下面的Interface介面。如下示例:
在router目錄下定義了一個CustomRouter結構體,該結構體實現了Interface介面

package router

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

type CustomRouter struct {
    server server.HttpServer
}

func NewCustom(srv server.HttpServer) *CustomRouter {
    return &CustomRouter{
        srv,
    }
}

func (*CustomRouter) Add(srv *gin.Engine) {
    srv.GET("/custom", func(ctx *gin.Context) {
        ctx.String(http.StatusOK, "custom router")
    })
}

需要注意的是,如果是自定義路由註冊,需要修改專案cmd資料夾下面的main.go入口檔案,透過http.SetRouters(router.NewCustom(http))註冊給gin

中介軟體

定義中介軟體與gin框架一樣,該估計預設實現了panic異常的中介軟體,可以檢視internal/server/middleware資料夾中的exception.go檔案。

如果需要定義其他的中介軟體並載入註冊,可以將定義好的中介軟體透過server.HttpServer介面的SetMiddleware(middlewares ...middleware.Interface)方法註冊載入,
比如我們實現如下自定義全域性中介軟體middleware/custom.go

type Custom struct{}

func (c *Custom) Handle() gin.HandlerFunc {
    return func(ctx *gin.Context) {
        fmt.Println("Custom middleware exec...")
    }
}

然後在定義路由的地方使用server.SetMiddleware(&middleware.Custom{})註冊中介軟體。
定義全域性路由中介軟體可以參考router/router.go中的New方法。

如果是區域性中介軟體,可以直接在具體的路由上註冊,參考gin路由中介軟體的用法

日誌

在該骨架中的日誌是直接對go.uber.org/zap的封裝,使用時,直接透過全域性變數variable.Log訪問寫入日誌,可直接使用zap支援的所有方法。

package demo
import "skeleton/internal/variable"
func Demo() {
    variable.Log.Info("info message")
}

日誌檔案預設是以json格式寫入到storage/logs/system.log裡面

配置

配置項的定義直接在config/config.yaml檔案中定義,並且配置的讀取寫入是透過封裝github.com/spf13/viper實現,在該骨架中,只提供瞭如下一些獲取配置的方法:

type ConfigInterface interface {
	Get(key string) any
	GetString(key string) string
	GetBool(key string) bool
	GetInt(key string) int
	GetInt32(key string) int32
	GetInt64(key string) int64
	GetFloat64(key string) float64
	GetDuration(key string) time.Duration
	GetStringSlice(key string) []string
}

需要注意的是,骨架中對配置項的獲取做了快取的處理,第一次載入是在檔案中獲取,後面每次回去都是在cache中獲取,目前cache預設只支援memory,骨架中也支援自定義cache的方法,只需要實現config.CacheInterface介面就可以,比如需要使用redis作為配置快取,可以透過下面的方式處理:

type ConfigRedisCache struct {}

var _ config.CacheInterface = (*ConfigRedisCache)(nil)

func (c *ConfigRedisCache) Get(key string) any {
    return nil
}

func (c *ConfigRedisCache) Set(key string, value any) bool {
    return true
}

func (c *ConfigRedisCache) Has(key string) bool {
    return true
}

func (c *ConfigRedisCache) FuzzyDelete(key string) {
    
}

然後將ConfigRedisCache結構體配置到config.Options中,如下所示,修改internal/bootstrap/init.go初始化配置的方法:

variable.Config, err := config.New(driver.New(), config.Options{
	BasePath: './',
    Cache: &ConfigRedisCache{}
})

config.yaml基礎配置如下:

# http配置
HttpServer:
  Port: ":8888"
  
  # 服務模式,和gin的gin.SetMode的值是一樣的
  Mode: "debug"
# socket配置
Websocket:
  WriteReadBufferSize: 2048
  HeartbeatFailMaxTimes: 4
  PingPeriod: 20
  ReadDeadline: 100
  WriteDeadline: 35
  PingMsg: "ping"
  
# 資料庫配置
Database:
  # 可以檢視GORM相關的配置選項
  Mysql:
    SlowThreshold: 5
    LogLevel: 4
    ConnMaxLifetime: 1
    MaxIdleConn: 2
    MaxOpenConn: 2
    ConnMaxIdleTime: 12
    Reade:
      - "root:root@tcp(192.168.1.4:3306)/test?charset=utf8mb4&loc=Local&parseTime=True"
    Write: "root:root@tcp(192.168.1.4:3306)/test?charset=utf8mb4&loc=Local&parseTime=True"
  # mongo資料庫的基礎配置
  Mongo:
    Enable: false
    Uri:
    MinPoolSize: 10
    MaxPoolSize: 20


Redis:
  Disabled: false
  Addr: "192.168.1.4:6379"
  Pwd: ""
  Db: 0
  PoolSize: 20
  MaxIdleConn: 30
  MinIdleConn: 10
  # 單位(秒)
  MaxLifeTime: 60
  # 單位(分)
  MaxIdleTime: 30

# 定時任務
Crontab:
  Enable: true

# 訊息佇列,使用rocketmq
MQ:
  Enable: false
  Servers:
    - "127.0.0.1:9876"
  ConsumptionSize: 1
  Retries: 1

事件機制

  • 定義事件實體

    app/event/entity目錄下定義一個事件實體,該實體實現了event.EventInterface介面:

    package entity
    
    type DemoEvent struct {}
    
    func (d *DemoEvent) EventName() string {
        return "demo-event"
    }
    
    func (d *DemoEvent) GetData() any {
        return "demo param"
    }
    
  • 定義事件監聽

    app/event/listen目錄中定義一個DemoEventListen事件監聽,並且該DemoEventListen結構體必須要實現event.Interface介面:

    package listen
    
    import (
    	"fmt"
    	event2 "skeleton/app/event/entity"
    	"skeleton/internal/event"
    )
    
    type DemoEventListen struct {
    }
    
    func (*DemoEventListen) Listen() event.EventInterface {
    	return &event2.DemoEvent{}
    }
    
    func (*DemoEventListen) Process(data any) (any, error) {
    	return fmt.Sprintf("%v --> %s", data, "exec DemoEventListen.Process"), nil
    }
    
    
  • 最後需要將事件進行註冊,在app/event/event.go檔案中的Init方法內執行:

    variable.Event.Register(&listen.DemoEventListen{})
    
  • 呼叫事件執行

    variable.Event.Dispatch(&entity.DemoEvent{})
    

驗證器

gin框架本身內建了validator校驗,骨架裡面只是對其引數的校驗做了統一的校驗入口。

透過如下方式獲取進行引數的校驗,並設定中文錯誤提示:

type Param struct {
    Name  int    `binding:"required" form:"name" query:"name" json:"name"`
}
appRequest, err := AppRequest.New("zh")
if err != nil {
    return
}
var data Param
errMap := appRequest.Validator(ctx, &data)
fmt.Println(errMap)

骨架裡面已經實現了預設的引數校驗,可以在app/request/request.go檔案中檢視。並且在controller目錄中base.go有一個Validate(ctx *gin.Context, param any)方法,在其他controller中要進行引數校驗的時候,只需要繼承base結構體,然後呼叫Validate方法。

package controller

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

type DemoController struct {
    base
}

type DemoRequest struct {
    Id int `binding:"required" form:"id" query:"id" json:"id"`
}

func (d *DemoController) Index(ctx *gin.Context) {
    var param DemoRequest
    if err := d.base.Validate(ctx, &param); err == nil {
		ctx.JSON(http.StatusOK, gin.H{"data": param})
	} else {
		ctx.JSON(http.StatusBadRequest, gin.H{"message": err})
	}
}

驗證規格參考github.com/go-playground/validator官方文件

命令列

基於github.com/spf13/cobra封裝

  • 定義命令

    app/command目錄中定義自己的命令,比如自定義一個輸出success ok的命令

    package command
    
    import (
        "fmt"
        "github.com/spf13/cobra"
    )
    
    type FooCommand struct {}
    
    func (f *FooCommand) Command() *cobra.Command {
        return &cobra.Command{
    		Use:   "foo",
    		Short: "命令使用簡介.",
    		Long: `命令介紹.`,
    		Run: func(cmd *cobra.Command, args []string) {
    			str, _ := cmd.Flags().GetString("name")
                 fmt.Printf("success, %s", str)
    		},
    	}
    }
    
    func (f *FooCommand) Flags(root *cobra.Command) {
    	root.PersistentFlags().String("name", "", "命令引數")
    }
    
  • 註冊命令

    需要在cmd/cli/cli.go中的main方法內註冊自定義命令。

  • 執行命令

    go run cmd/cli/cli.go foo --name ok
    
  • 檢視命令資訊

    go run cmd/cli/cli.go help
    
    // 或者
    go run cmd/cli/cli.go foo --help
    

定時任務

定時是透過封裝github.com/robfig/cron/v3實現

  • 定義定時任務方法

    app/task目錄下定義執行方法,比如每一分鐘列印success字元

    package task
    
    import "fmt"
    
    type SuccessTask struct {
    }
    
    // 時間規則
    func (s *SuccessTask) Rule() string {
    	return "* * * * *"
    }
    
    func (s *SuccessTask) Execute() func() {
    	return func() {
    		fmt.Println("success")
    	}
    }
    
  • 載入定時任務

    需要在app/task/task.go檔案中的Tasks方法內,載入自定義的任務,參考task目錄下的task.go檔案

相關文章