节省时间与精力,更高效地打造稳固牢靠的Web我的项目:基于Go语言和Gin框架的欠缺Web我的项目骨架。无需从零开始,间接利用这个骨架,疾速搭建一个功能齐全、性能优异的Web利用。充分发挥Go语言和Gin框架的劣势,轻松解决高并发、大流量的申请。构建可扩展性强、易于保护的代码架构,保障我的项目的长期稳固运行。同时,通过集成罕用功能模块和最佳实际,缩小繁琐的开发工作,使您专一于业务逻辑的实现。

该骨架每个组件之间可独自应用,组件之间松耦合,高内聚,组件的实现基于其余三方依赖包的封装。
目前该骨架实现了大多数的组件,比方事件,中间件,日志,配置,参数验证,命令行,定时工作等性能,目前能够满足大多数开发需要,后续会继续保护更新性能。

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

设置环境变量并下载我的项目依赖

go env -w GO111MODULE=ongo env -w GOPROXY=https://goproxy.cn,directgo 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 ---> 单元测试目录

骨架全局变量

该骨架中全局变量如下,可间接查看internal/variable.go文件。

var (  // 我的项目更目录  BasePath string  // Log日志  Log      *zap.Logger  // 配置,Viper封装  Config   *config.Config  // 数据库Gorm  DB       *gorm.DB  MongoDB  *mongo.MongoDB  Redis    *redis.Client  Crontab  *crontab.Crontab  MQ       *mq.RocketMQ  // 事件  Event    *event.Event)

根底性能


路由

该骨架的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 routerimport (    "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 demoimport "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: 20Redis:  Disabled: false  Addr: "192.168.1.4:6379"  Pwd: ""  Db: 0  PoolSize: 20  MaxIdleConn: 30  MinIdleConn: 10  # 单位(秒)  MaxLifeTime: 60  # 单位(分)  MaxIdleTime: 30# 定时工作Crontab:  Enable: true# 音讯队列,应用rocketmqMQ:  Enable: false  Servers:    - "127.0.0.1:9876"  ConsumptionSize: 1  Retries: 1

事件机制

  • 定义事件实体

    app/event/entity目录下定义一个事件实体,该实体实现了event.EventInterface接口:

    package entitytype 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 listenimport (    "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 ParamerrMap := appRequest.Validator(ctx, &data)fmt.Println(errMap)

骨架外面曾经实现了默认的参数校验,能够在app/request/request.go文件中查看。并且在controller目录中base.go有一个Validate(ctx *gin.Context, param any)办法,在其余controller中要进行参数校验的时候,只须要继承base构造体,而后调用Validate办法。

package controllerimport "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 commandimport (    "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 taskimport "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文件

Websocket

  • 音讯解决与链接敞开监听

    该骨架中的websocket是对github.com/gorilla/websocket依赖库的封装,在编写通信性能时,只须要实现websocket.MessageHandler接口:

    import     (  AppSocket "skeleton/internal/server/websocket")type socketHandler struct {}// 音讯解决func (s *socketHandler) OnMessage(AppSocket.Message) {  fmt.Println(fmt.Sprintf("mt: %v,data: %s, uuids: %v", message.MessageType, message.Data, message.Subkeys))}func (s *socketHandler) OnError(err error) {  fmt.Println(fmt.Sprintf("socket err: %s", err))}func (s *socketHandler) OnClose() {  fmt.Println("socket closed.")}
  • 创立链接Websocket

    import     (  "github.com/google/uuid"  "github.com/gin-gonic/gin"  AppSocket "skeleton/internal/server/websocket")var client AppSocket.SocketClientInterfacefunc init() {  client, _ = AppSocket.NewSocket(AppSocket.WithHandler(&socketHandler{}))}func Connect(ctx *gin.Context) {  subkey := uuid.New().String()    client.Connect(ctx, subkey)}
  • 发送音讯

    这里能够发送全副信息给全副用户,或者发送给用户,AppSocket.Message构造体中Subkeys []string示意须要发送给哪些用户:

    client.WriteMessage(AppSocket.Message{      MessageType: websocket.TextMessage,      Data:        []byte("服务端收到音讯并回复ok"),      Subkeys:     []string{"9bae9c4f-87a8-46b1-b1b9-4f37b63a7d19"},  })
    如果Subkeys是空切片数组,会将音讯推送给全副在线用户
  • 心跳音讯

    websocket标准协议实现隐式心跳,Server端向Client端发送ping格局数据包,浏览器收到ping规范格局,主动将音讯原路返回给服务器

  • 其余办法

    • GetAllKeys() []string:获取所有websocket连贯uuid
    • GetClientState(key string) bool:获取指定客户端在线状态