MCPcopy Index your code
hub / github.com/czx-lab/skeleton

github.com/czx-lab/skeleton @v1.0.1

Chat with this repo
repository ↗ · DeepWiki ↗ · release v1.0.1 ↗ · + Follow
666 symbols 1,391 edges 76 files 48 documented · 7%
What it actually does AI analysis from the code graph — generated when you open this
loading…
README

节省时间与精力,更高效地打造稳定可靠的Web项目:基于Go语言和Gin框架的完善Web项目骨架。无需从零开始,直接利用这个骨架,快速搭建一个功能齐全、性能优异的Web应用。充分发挥Go语言和Gin框架的优势,轻松处理高并发、大流量的请求。构建可扩展性强、易于维护的代码架构,保证项目的长期稳定运行。同时,通过集成常用功能模块和最佳实践,减少繁琐的开发工作,使您专注于业务逻辑的实现。

该骨架每个组件之间可单独使用,组件之间松耦合,高内聚,组件的实现基于其他三方依赖包的封装。 目前该骨架实现了大多数的组件,比如事件,中间件,日志,配置,参数验证,命令行,定时任务等功能,目前可以满足大多数开发需求,后续会持续维护更新功能。

单元测试

go test -v -run=TestConfig ./test

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

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文件。 ```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

// RabbitMQ Amqp mq.RabbitMQInterface

// rocketmq, 目前官方RocketMQ Golang SDK一些功能尚未完善,暂时不可用 MQ mq.Interface

// 事件 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!")
})

也可以通过自己定义路由的定义注册,只需要实现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: true
  Servers:
    - "127.0.0.1:9876"
  Retries: 1
  ProducerGroupName: "ProducerGroup"
  ConsumerGroupName: "ConsumerGroup"

# RabbitMQ
Amqp:
  Enable: true
  Addr: "amqp://guest:guest@127.0.0.1:5672/"  

事件机制

  • 定义事件实体

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

```go package entity

type DemoEvent struct {}

func (d *DemoEvent) GetData() any { return "demo param" } ```

  • 定义事件监听

app/event/listen目录中定义一个DemoEventListen事件监听,并且该DemoEventListen结构体必须要实现event.Interface接口:

```go package listen

import ( "fmt" event2 "skeleton/app/event/entity" "skeleton/internal/event" )

type DemoEventListen struct { }

// 可同时监听多个事件 func (*DemoEventListen) Listen() []event.EventInterface { return []event.EventInterface{&event2.DemoEvent{}} }

func (*DemoEventListen) Process(data any) { fmt.Printf("%v --> %s", data, "exec DemoEventListen.Process") } ```

  • 最后需要将事件进行注册,在app/event/event.go文件中的Init方法内执行:

go variable.Event.Register(&listen.DemoEventListen{})

  • 调用事件执行

```go // 同步 variable.Event.Dispatch(&entity.DemoEvent{})

// 异步 variable.Event.DispatchAsync(&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封装,骨架实现了基于gorm的gen代码生成器命令行。

  • 生成数据库model与dao方法命令

go run cmd/cli/main.go gen:model

关于代码生成命令行的配置,可参考app/command/command.go文件下的newGenCommand() AppCommand.Interface方法

  • 自定义定义命令

app/command目录中定义自己的命令,比如自定义一个输出success ok的命令

```go 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.Flags().StringP("name", "n", "world!", "命令参数") } ```

  • 注册命令

需要在app/command/command.go中的RegisterCmds() []AppCommand.Interface方法内注册自定义命令,具体使用方法可查看原文件app/command/command.go

  • 全局标志

需要在app/command/command.go中的GlobalFlags()方法内命令,具体使用方法可查看原文件app/command/command.go

  • 执行命令

go go run cmd/cli/main.go foo --name ok

  • 查看命令信息

```go go run cmd/cli/main.go help

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

定时任务

定时是通过封装github.com/robfig/cron/v3实现

  • 定义定时任务方法

app/task目录下定义执行方法,比如每一分钟打印success字符

```go 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接口:

```go import ( AppSocket "skeleton/internal/server/websocket" )

type socketHandler struct {}

// 消息处理 func (s *socketHandler) OnMessage(message AppSocket.Message) { fmt.Println(fmt.Sprintf("mt: %v,data: %s, uuids: %v", message.MessageType, message.Data, message.Subkeys)) }

func (s *socketHandler) OnError(key string, err error) { fmt.Printf("socket err: %s, client: %s", err, key) }

func (s *socketHandler) OnClose(key string) { fmt.Printf("socket closed. client:%s", key) } ```

  • 创建链接Websocket

```go 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表示需要发送给哪些用户:

go 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) ClientState:获取指定客户端在线状态

消息中间件

RabbitMQ

RabbitMQ消息中间件的使用可参考test/rmq_test.go单元测试,同时骨架中也实现了RabbitMQ的简单模式示例:app/amqp目录下可查看

全局变量variable.Amqp返回mq.RabbitMQInterface接口,接口中的方法可查看源文件查看

  • 定义消费者consumer

定义consumer,需要实现mq.ConsumerHandler接口,该接口可查看internal/mq/rabbitmq.go文件中的定义,比如定义一个简单模式的消费者:

```go import ( "fmt" "skeleton/internal/mq"

"github.com/streadway/amqp"

)

type FooConsumer struct{}

func (*FooConsumer) Option() mq.ConsumerOption { return mq.ConsumerOption{ CommonOption: mq.CommonOption{ Mode: mq.SimpleMode, QueueName: "foo", Durable: false, AutoDelete: false, Exclusive: false, NoWait: false, Args: nil, }, AutoAck: true, NoLocal: false, } }

func (*FooConsumer) Exec(msg <-chan amqp.Delivery) { for v := range msg { fmt.Printf("consumer message:%v\n", string(v.Body)) } } ```

当我们定义好消费者后,需要通过mq.InitConsumer中的InitConsumers() []ConsumerHandler进行注册,骨架中注册文件已写死,项目启动时,会执行InitConsumers方法,查看app/amqp/amqp.go文件的代码:

```go package amqp

import ( "skeleton/app/amqp/consumer" "skeleton/internal/mq" )

type Amqp struct{}

func (*Amqp) InitConsumers() []mq.ConsumerHandler { return []mq.ConsumerHandler{ &consumer.FooConsumer{}, } } ```

  • 发送消息producer

通过全局变量variable.Amqp的方法发送消息:

go opts := mq.ProducerOption{ CommonOption: mq.CommonOption{ Mode: mq.SimpleMode, QueueName: "foo", Durable: false, AutoDelete: false, Exclusive: false, NoWait: false, Args: nil, }, Message: amqp.Publishing{ ContentType: "text/plain", Body: []byte("test message"), }, Mandatory: false, Immediate: false, } variable.Amqp.Publish(opts)

RocketMQ

注意:目前官方RocketMQ Golang SDK一些功能尚未完善,暂时不可用

消息中间件的使用可参考test/mq_test.go单元测试

骨架对外提供了mq.Interface接口中的方法,可查看internal/mq/rocketmq.go文件下的Interface接口定义

定义consumer时,只需要实现mq.ConsumerInterface接口即可,详见mq_test单元测试

Extension points exported contracts — how you extend this code

ConsumerHandler (Interface)
(no doc) [4 implementers]
internal/mq/rabbitmq.go
ContainerInterface (Interface)
ContainerInterface 容器相关方法 [2 implementers]
internal/constants/container/interface.go
EventInterface (Interface)
(no doc) [3 implementers]
internal/event/event.go
UserMethod (Interface)
only used to User [1 implementers]
app/interface/dao/user-interface.go
IValidator (Interface)
(no doc) [2 implementers]
internal/request/request.go
IAppConfig (Interface)
(no doc) [2 implementers]
internal/config/config.go
TaskInterface (Interface)
(no doc) [1 implementers]
internal/crontab/crontab.go
CommandInterface (Interface)
(no doc) [1 implementers]
internal/command/command.go

Core symbols most depended-on inside this repo

String
called by 19
internal/mq/rabbitmq.go
Get
called by 16
internal/constants/container/interface.go
Printf
called by 14
internal/database/logger/log.go
Flags
called by 11
internal/command/command.go
GetString
called by 11
internal/constants/config/config_interface.go
Add
called by 9
internal/server/router/router.go
Get
called by 8
internal/config/config.go
Publish
called by 5
internal/mq/rabbitmq.go

Shape

Method 409
Function 120
Struct 85
Interface 36
FuncType 11
TypeAlias 5

Languages

Go100%

Modules by API surface

app/dao/user.gen.go130 symbols
internal/mq/rocketmq.go34 symbols
internal/server/websocket/socket.go30 symbols
internal/mq/rabbitmq.go26 symbols
internal/elasticsearch/elastic.go23 symbols
internal/server/http.go21 symbols
internal/command/gorm-gen-command.go18 symbols
app/dao/gen.go17 symbols
internal/logx/options.go16 symbols
internal/config/config.go16 symbols
test/mq_test.go15 symbols
internal/redis/redis.go14 symbols

For agents

$ claude mcp add skeleton \
  -- python -m otcore.mcp_server <graph>

⬇ download graph artifact