用gin框架配合golang方面比较优秀的库,搭建的一个项目结构,方便快速开发项目。出结果 用最少的依赖实现80%项目可以完成的需求
- 使用主流轻量的路由框架gin,实现路由
- 引入
github.com/go-playground/validator实现常见的验证,最重要的是引入了中文的提示,以及可以自定义字段名字 - 引入主流的
gorm库作为数据库层的操作 - 引入
github.com/redis/go-redis/v9作为缓存层操作 - 引入
github.com/google/uuid生成traceid,traceid贯穿于各种日志,以及在响应中返回,并且支持自定义traceid的字段名字 - 引入
github.com/labstack/gommon实现调试模式下日志打印到console,并且不同的日志级别用不用的颜色进行区分 - 引入
github.com/robfig/cron实现定时任务,定时任务也引入了traceid - 使用轻量的日志库
github.com/rs/zerolog进行记录日志 - 引入
gopkg.in/yaml.v3解析yaml配置文件到golang变量 - 引入
github.com/go-resty/resty/v2发起http请求,方便的请求第三方接口 - 引入
github.com/hibiken/asynq实现异步队列 - 引入
github.com/hibiken/asynqmon队列使用情况的漂亮web ui界面 - 引入
gorm.io/gorm操作数据库
- 封装了db error,redis error, db层减少了很多if error,else error的判断
- 封装了gin框架,减少了控制器层error相关if else的判断,使代码更美观,清晰,简洁
- 封装了gin的参数绑定,提供了shouldbindhandle、shouldbindqueryhandle等方法,使controller代码更优雅
- crontab、task、event、数据库迁移migration可以优雅的使用
- 提供了状态相关的枚举结构
- 日志记录traceid,方便排查问题
github.com/gin-gonic/gin v1.9.1
github.com/go-playground/locales v0.14.1
github.com/go-playground/universal-translator v0.18.1
github.com/go-playground/validator/v10 v10.14.0
github.com/go-resty/resty/v2 v2.13.1
github.com/go-sql-driver/mysql v1.8.1
github.com/golang-module/carbon/v2 v2.3.12
github.com/google/uuid v1.6.0
github.com/hibiken/asynq v0.24.1
github.com/hibiken/asynqmon v0.7.2
github.com/labstack/gommon v0.4.2
github.com/redis/go-redis/v9 v9.7.0
github.com/robfig/cron/v3 v3.0.1
github.com/rs/zerolog v1.33.0
golang.org/x/text v0.16.0
gopkg.in/yaml.v3 v3.0.1
gorm.io/driver/mysql v1.5.7
gorm.io/gorm v1.25.10- cmd/ - web服务、cron的主入口目录
- config/ -配置文件目录
- const/ -常量目录
- errcode - 错误结构
- enum - 枚举常量结构
- controller/ - 控制器目录
- internal/ -内部功能目录,里面方法不建议修改
- cron/ - 定时任务目录
- middleware/ -中间件目录
- model/ -数据层目录
- entity/ - 表结构映射(强类型,GORM models)
- do/ - Data Object(any 类型,用于 Where/Data 条件构建)
- dao/ - Data Access Object(单例模式,链式调用 API)
- logic/ -业务逻辑目录
- typing/ 结构目录,用于定义请求参数、响应的数据结构
- util/ 工具目录,提供常用的辅助函数,一般不包含业务逻辑和状态信息
- event/ 事件目录
- listener/ 事件监听器
- rest/ 请求第三方服务的目录
- task/ 任务队列目录
- router/ 路由目录
- migration/ 数据库迁移目录
-
控制器
在
controller目录下面创建控制器,例如user_controller.gotype userController struct { } var UserController = &userController{ } func (c *userController) List(ctx *httpx.Context) (any, error) { var req types.ListReq l := logic.NewGetUsersLogic() return l.Handle(ctx, req) }
然后在
controllers/init.go文件定义路由即可user_router := route.Group("/user") user_router.GET("/", UserController.Index)
控制器直接返回logic层处理后的结果,不需要关心响应格式,减少了不必要的if,else判断,自己封装的gin框架底层会根据error自动判断渲染数据还是error数据
封装响应的原因是定义了输出的响应结构,如下,永远返回包含code、data、message、trace_id四个字段的结构,使响应结果结构化 ```shell { "code": 0, "data": { "data": "add user succcess ddddd=96" }, "message": "操作成功", "trace_id": "dc119c64-d4b9-4af1-9e02-d15fc4ba2e42" }如果响应结构字段名字不符合你的预期,可以进行自定义
func main() { // to do something httpx.DefaultSuccessCodeValue = 0 // 定义成功的code默认值,默认是0,你也可以改成200 httpx.DefaultSuccessMessageValue = "成功" // 定义成功的message默认值,默认是'操作成功' httpx.CodeFieldName = "code" // 定义响应结构的code字段名,你也可以改成status httpx.MessageFieldName="msg"// 定义响应结构的消息字段名 httpx.ResultFieldName = "data"// 定义响应结构的数据字段名 traceid.TraceIdFieldName="request_id" // 定义响应以及日志中traceid的字段名字 }
响应结果类似如下
{ "code": 10001, "data": null, "msg": "年龄为必填字段\n", "request_id": "8ddb97db-be44-4df0-8110-0d38a0cc4657" } -
业务层
业务层采用命令模式,一个logic只负责处理一个业务的处理,例如
getusers_logic.gotype GetUsersLogic struct{} func NewGetUsersLogic() *GetUsersLogic { return &GetUsersLogic{} } func (l *GetUsersLogic) Handle(ctx context.Context, req typing.ListReq) (*typing.ListReply, error) { var users []entity.User err := dao.User.Ctx(ctx). Where(do.User{Status: 1}). Order("id DESC"). All(&users) if err != nil { return nil, err } return &typing.ListReply{Users: users}, nil }
-
数据库
推荐使用 GoFrame 风格的 DAO 链式调用 API:
// 查询 var users []entity.User err := dao.User.Ctx(ctx). Where(do.User{Status: 1}). Where("age > ?", 18). Order("id DESC"). Page(1, 10). All(&users) // 插入 _, err := dao.User.Ctx(ctx). Data(do.User{Name: "test", Status: 1}). Insert() // 更新 _, err := dao.User.Ctx(ctx). Data(do.User{Status: 2}). Where(do.User{Id: 1}). Update() // 删除 _, err := dao.User.Ctx(ctx). Where(do.User{Id: 1}). Delete() // 使用 Columns 常量避免硬编码字段名 cols := dao.User.Columns() err := dao.User.Ctx(ctx). Fields(cols.Id, cols.Name). Where(cols.Status+" = ?", 1). All(&users)
底层仍可使用
db.WithContext(ctx)直接操作 GORM(仅在 DAO 内部使用):db.WithContext(ctx).Find(&u).Error()
Error()方法底层转成DBError,便于上层区分,以及响应判断
-
redis
系统的redis库用的是
go-redis,没有进行过多的封装,获取redis连接后,使用方法上就跟go-redis一样了,调用Client()方法获取redis资源对象redisx.Client().HSet(ctx, "name", "age", 43)
-
日志
系统提供了debug、info、warn、error四种级别的日志,接口如下
type Logger interface { Debug(keyword string, message any) Debugf(keyword string, format string, message ...any) Info(keyword string, message any) Infof(keyword string, format string, message ...any) Warn(keyword string, message any) Warnf(keyword string, format string, message ...any) Error(keyword string, message any) Errorf(keyword string, format string, message ...any) }
可以通过env文件指定日志存储路径和要记录的日志级别,使用方式如下,第一个参数是用于为要记录的日志起一个有意义的关键字,便于grep日志
logx.WithContext(ctx).Warn("ShouldBind异常", err) logx.WithContext(ctx).Warnf("这是日志%s", "我叫张三")
最终日志文件中记录的内容如下格式,包含
trace_id{"level":"WARN","keyword":"redis","data":"services/user_service.go:24 execute command:[hset name age 43], error=dial tcp 192.168.65.254:6379: connect: connection refused","time":"2024-06-22 23:24:10","trace_id":"5f8b1ee9-7daf-4269-806a-029ee7c3768f"}另外,常规日志文件的名字是
年-月-日.log格式,如2024-05-22.log。值得注意的是warn、error级别日志会单独拿到年-月-日-error.log格式文件,如2024-05-22-error.log,这样一方面是便于很好的监控异常,另一方面可以很快的排查异常问题此外,系统还提供记录请求access日志,会记录到env配置的路径下的access文件夹,文件以
年-月-日.log格式命名,日志内容主要包含请求路径、get参数、请求Method、响应码、耗时、User-Agent几个重要参数,格式如下{"level":"INFO","path":"/user/list","method":"GET","ip":"127.0.0.1","cost":"227.238215ms","status":200,"proto":"HTTP/1.1","user_agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36","time":"2024-06-22 23:28:20","trace_id":"f606d909-2f4c-4455-b4b9-5eea0684c49a"} -
事件 事件定义在
event/目录,事件的监听器定义在event/listener/目录 事件参考内容如下,使用eventbus.NewEvent方法注册事件package event import ( "go-gin/internal/eventbus" ) var SampleEventName = "event.sample" func NewSampleEvent(user string) *eventbus.Event { return eventbus.NewEvent(SampleEventName, user) }
事件监听器
listener参考内容如下,只需要实现Handle方法package listener import ( "context" "fmt" "go-gin/internal/eventbus" "go-gin/model/entity" ) type DemoAListener struct { } func (l DemoAListener) Handle(ctx context.Context, e *eventbus.Event) error { user := e.Payload().(*entity.User) fmt.Println(user.Name) return nil }
在
event/init.go文件进行事件与监听器的绑定,使用eventbus.AddListener方法`进行绑定,一个事件可以有多个监听器,如果前一个监听器返回error,后面的监听器不会执行eventbus.AddListener(SampleEventName, &listener.SampleAListener{}, &listener.SampleBListener{})
在控制器触发事件,触发方式
eventbus.Fire(ctx, event.NewSampleEvent("hello 测试")) // 或者 event.NewSampleEvent("333").Fire(ctx)
-
定时任务
定时任务的入口文件为
cmd/cron/main.go,具体业务代码在cron目录编写。定时任务业务代码可以像api模式一样使用log、db定义一个job首先要定义一个实现了
cronx.Job的接口的结构,cronx.Job接口如下type Job interface { Handle(ctx context.Context) error // 实现业务逻辑 }
例子如下
type SampleJob struct{} func (j *SampleJob) Handle(ctx context.Context) error { var u entity.User dao.User.Ctx(ctx).Where(do.User{Id: 1}).One(&u) return nil }
然后在
cron/init.go文件定义cron的任务执行频率即可,如下定义SampleJob每3s执行一次cronx.AddJob("@every 3s", &SampleJob{}) // cronx.Schedule(&SampleJob{}).EveryMinute() // cronx.Schedule(&SampleJob{}).EveryFiveMinutes()提供了
EveryFiveMinutes、EveryThreeMinutes、EveryTenMinutes等多种优雅的时候方法 定时任务也支持func方式,只需要提供一个cronx.JobFunc类型的函数即可,也就是func(context.Context) error形式 例子如下:func SampleFunc(ctx context.Context) error { fmt.Println("this is a sample function") return nil }
我只需要像注册结构体方式一样,将func添加的定时任务管理器即可
cronx.AddFunc("@every 5s", SampleFunc) cronx.ScheduleFunc(SampleFunc).EveryMinute()
定时任务其它执行频率的定义方式可以参考https://github.com/robfig/cron
-
验证器
验证器主要是对
gin内置的binding进行的扩展,支持中文化提示和自定义字段名type AddUserReq struct { Name string `form:"name" binding:"required"` Age int `form:"age" binding:"required" label:"年龄"` Status bool `form:"status"` }
使用
label标签定义字段名字,验证失败时提示年龄为必填字段而不是Age为必填字段验证失败返回
BizError类型错误,错误码默认10001 -
参数、响应结构
请求参数和响应结构定义在
typing/目录,一个模块一个文件名,如user.gotype AddUserReq struct { Name string `form:"name" binding:"required"` Age int `form:"age" binding:"required" label:"年龄"` } type AddUserReply struct { Message string `json:"message"` }
Controller 使用
httpx.ShouldBindHandle自动绑定参数并调用 Logic:func (c *userController) AddUser(ctx *httpx.Context) (any, error) { return httpx.ShouldBindHandle(ctx, logic.NewAddUserLogic()) }
-
常量
业务错误常量定义在
const/errcode目录:var ErrUserNotFound = errorx.New(20001, "用户不存在")
-
错误类型
BizError: 业务错误,HTTP 200,自定义 code -errorx.New(20001, "用户不存在")ServerError: 服务错误,返回对应 HTTP 状态码 -errorx.NewServerError(http.StatusNotFound)- 业务错误码建议从 20000 开始(10000-20000 保留给框架)
-
枚举常量
- 枚举常量定义在
const/enum目录 - 定义枚举只需两步:
- 定义结构体和常量:
// const/enum/user_status.go package enum import "go-gin/internal/etype" type UserStatus struct { etype.BaseEnum } var ( USER_STATUS_NORMAL = etype.NewEnum[UserStatus](1, "正常") USER_STATUS_DISABLED = etype.NewEnum[UserStatus](2, "禁用") )
- 运行代码生成器自动生成
gen_*.go文件(包含 Scan/Value/JSON 方法):go run ./cmd/make/... make:enum
- 定义结构体和常量:
- 使用示例:
// Entity 中使用枚举 type User struct { Id int64 `gorm:"column:id" json:"id"` Name string `gorm:"column:name" json:"name"` Status *enum.UserStatus `gorm:"column:status" json:"status"` } // 创建记录 user := entity.User{Name: "test", Status: enum.USER_STATUS_NORMAL} dao.User.Ctx(ctx).Data(&user).Insert() // 查询时自动解析枚举 var u entity.User dao.User.Ctx(ctx).Where(do.User{Id: 1}).One(&u) fmt.Println(u.Status.Desc()) // 输出: 正常 // JSON 序列化只输出 code 值 // {"id":1,"name":"test","status":1}
- 枚举常量定义在
-
请求第三方接口
使用
httpc库(基于 go-resty 封装):resp, err := httpc.POST(ctx, "http://localhost:8080/api/list"). SetFormData(httpc.M{"username": "test"}). Send()
复杂的第三方服务可在
rest/目录定义服务接口 -
队列
队列使用
github.com/hibiken/asynq库,队列服务入口为cmd/queue,任务代码维护在task/目录// 发送任务 task.DispatchNow(task.NewSampleTask("测试")) // 立即执行 task.Dispatch(task.NewSampleTask("测试"), 3*time.Second) // 延迟3s执行 // 在 task/init.go 注册 handler task.Handle(NewSampleTaskHandler())
-
util方法
util.IsTrue/IsFalse- 标量判断util.When(condition, trueValue, falseValue)- 三元表达式jsonx.Encode/Decode- JSON 序列化/反序列化
1. git clone git@github.com:fanqingxuan/go-gin.git
2. cd go-gin && go mod tidy
3. go run cmd/api/main.go -f .env // api启动方式
4. go run cmd/cron/main.go -f .env // 定时任务启动方式
5. go run cmd/queue/main.go -f .env // 队列服务入口
6. go run cmd/migrate/main.go -f .env // 数据库迁移入口统一的代码生成工具 cmd/make/,注意必须使用 ./cmd/make/... 语法:
# 生成枚举方法 (扫描 const/enum/ 生成 gen_*.go)
go run ./cmd/make/... make:enum
# 生成 entity/do/dao 三层代码 (从数据库表结构)
go run ./cmd/make/... make:dao -f .env
go run ./cmd/make/... make:dao -f .env -t user,order # 指定表
# 生成迁移文件模板
go run ./cmd/make/... make:migration create_orders