乐趣区

关于go:基于go语言gin框架的web项目骨架

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

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

github 地址:https://github.com/czx-lab/skeleton
gitee 地址:https://gitee.com/neddie/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 ---> 单元测试目录

骨架全局变量

该骨架中全局变量如下,可间接查看 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 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 文件

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.SocketClientInterface
    
    func 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: 获取指定客户端在线状态
退出移动版