用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/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-sql-driver/mysql v1.8.1
github.com/google/uuid v1.6.0
github.com/labstack/gommon v0.4.2
github.com/redis/go-redis/v9 v9.5.1
github.com/robfig/cron v1.2.0
github.com/rs/zerolog v1.33.0
golang.org/x/text v0.14.0
gopkg.in/yaml.v3 v3.0.1
gorm.io/driver/mysql v1.5.7
gorm.io/gorm v1.25.10
github.com/go-resty/resty/v2 v2.13.1
- cmd/ - web服务、cron的主入口目录
- config/ -配置文件目录
- consts/ -常量目录
- controllers/ - 控制器目录
- internal/ -内部功能目录,里面方法不建议修改
- crons/ - 定时任务目录
- middlewares/ -中间件目录
- models/ -数据表结构目录
- services/ -业务逻辑目录
- types/ 结构目录,用于定义请求参数、响应的数据结构
- utils/ 工具目录,提供常用的辅助函数,一般不包含业务逻辑和状态信息
- events/ 事件目录
- listeners/ 事件监听器
- rest/ 请求第三方服务的目录
- task/ 任务队列目录
-
控制器
在
controllers
目录下面创建控制器,例如user_controller.go
type userController struct { } var UserController = &userController{ } func (c *userController) Index(ctx *gin.Context) { httpx.Ok(ctx, "hello world") }
然后在
controllers/init.go
文件定义路由即可user_router := route.Group("/user") user_router.GET("/", UserController.Index)
另外,对于控制器的响应封装了几个公共方法
httpx.Ok(ctx, "hello world") // 输出正常的响应 httpx.OkWithMessage(ctx *gin.Context, data any, msg string) httpx.Error(ctx, err) //输出异常的响应 httpx.Handle(ctx *gin.Context, data any, err error) //既可以输出正常的响应,又可以说出异常的响应
封装响应的原因是定义了输出的响应结构,如下,永远返回包含code、data、message、trace_id四个字段的结构,使响应结果结构化
{ "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" }
-
服务层
服务层代码没有什么特别的,需要说明的是方法的第一个参数建议是
context.Context
,一是统一规范,二是可以日志记录traceidtype UserService struct { } func NewUserService() *UserService { return &UserService{} } func (svc *UserService) GetAllUsers(ctx context.Context) ([]models.User, error) { var u []models.User if err := db.WithContext(ctx).Find(&u).Error; err != nil { return nil, err } return u, nil }
-
数据库
要使用数据库,为了记录traceid,以及防止乱调用,所以系统只定义了一种获取gorm连接的方式,必须先调用
WithContext(ctx)
才能获得gorm资源,如下db.WithContext(ctx).Find(&u).Error
-
redis
系统的redis库用的是
go-redis
,没有进行过多的封装,获取redis连接后,使用方法上就跟go-redis
一样了,调用GetInstance()
方法获取redis资源对象redisx.GetInstance().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"}
-
定时任务
定时任务的入口文件为
cmd/cron/main.go
,具体业务代码在crons
目录编写。定时任务业务代码可以像api模式一样使用log
、db
定义一个job首先要定义一个实现了
cron.Job
的接口的结构,cron.Job
接口如下type Job interface { Name() string // 定义job的名称 Handle(ctx context.Context) error // 实现业务逻辑 }
例子如下
type SampleJob struct{} func (j *SampleJob) Name() string { return "sample job" } func (j *SampleJob) Handle(ctx context.Context) error { var u models.User db.WithContext(ctx).Find(&u) return nil }
然后在
crons/init.go
文件定义cron的任务执行频率即可,如下定义SampleJob
每3s执行一次cron.AddJob("@every 3s", &SampleJob{})
定时任务其它执行频率的定义方式可以参考https://github.com/robfig/cron
-
验证器
验证器主要是对
gin
内置的binding进行的扩展-
支持中文化提示
type AddUserReq struct { Name string `form:"name" binding:"required"` Age int `form:"age" binding:"required"` Status bool `form:"status"` Ctime time.Time `form:"ctime"` } // controller var req types.AddUserReq if err := ctx.ShouldBind(&req); err != nil { logx.WithContext(ctx).Warn("ShouldBind异常", err) httpx.Error(ctx, err) return }
如上如果参数不包括name的时候,会提示如下,自动进行了中文化处理
{"code":10001,"data":null,"message":"Name为必填字段\n年龄为必填字段\n","trace_id":"695517e3-1b68-4845-839d-c0e58d8f3a43"}
-
支持自定义提示语的字段名字
使用
label
标签定义字段名字type AddUserReq struct { Name string `form:"name" binding:"required"` Age int `form:"age" binding:"required" label:"年龄"` Status bool `form:"status"` Ctime time.Time `form:"ctime"` }
如上提示语不再提示
Age为必填字段
,而是提示年龄为必填字段
-
支持非
gin
框架方式使用验证器 提供了validators.Validate()
方法进行验证结构字段的值是否合理var req = types.AddUserReq{ Name: "测试", } if err := validators.Validate(&req); err != nil { httpx.Error(ctx, err) return }
注意:
validators.Validate
和ctx.ShouldBind
验证失败返回的是BizError
类型错误,错误码是ErrCodeValidateFailed
,默认值是10001
,你也可以通过errorx.ErrCodeValidateFailed = xxx
在main入口修改默认值
-
-
参数、响应结构
定义了可以规范化请求参数、响应结构的目录,使代码更容易维护,结构定义在
types/
目录,一个模块一个文件名,如user.go
结构定义如下
package types import ( "time" ) type AddUserReq struct { Name string `form:"name"` Age int `form:"age"` Status bool `form:"status"` Ctime time.Time `form:"ctime"` } type AddUserReply struct { Message string `json:"message"` }
使用方式,在
controller
层使用var req types.AddUserReq if err := ctx.ShouldBind(&req); err != nil { logx.WithContext(ctx).Warn("ShouldBind异常", err) httpx.Error(ctx, err) return }
其实就是使用了
gin
框架本身提供的shouldbind特性,将参数绑定到结构体,后面逻辑直接可以使用结构体里面的字段进行操作了,参数需要包括那些字段,通过结构体很容易看到,实现了参数的可维护性resp := types.AddUserReply{ Message: fmt.Sprintf("add user succcess %s=%d", user.Name, user.Id), } httpx.Ok(ctx, resp)
响应结构体如上,结构体数据响应中转成json渲染到
data
域,这样实现相应的结构化和可维护性,响应结果如下{"code":0,"data":{"message":"add user succcess ddddd=125"},"message":"成功","trace_id":"b1a9e4f8-7772-4c3a-bb3d-99a22d6a0ff6"}
-
常量
未来系统中可能会存在很多业务常量,这里预先建立了目录,当前内置了一些关于错误的预定义常量,这样在业务逻辑中直接使用即可,不需要到处写相同的错误,另外使错误相关更加集中,方便管理,也提高了可维护性
var ( ErrUserNotFound = errorx.New(2001, "用户不存在") )
-
错误类型 系统内置了两种错误类型
BizError
和ServerError
ServerError
主要是为了处理no method或者method not allowed以及其他服务上的错误,便于响应返回正确的http状态码和统一一致的响应结构,errorx
包内置错误常量
ErrMethodNotAllowed = NewServerError(http.StatusMethodNotAllowed) ErrNoRoute = NewServerError(http.StatusNotFound) ErrInternalServerError = NewServerError(http.StatusInternalServerError)
BizError
是我们业务开发中使用更多的错误结构,就是业务中定义的异常错误类型,这种类型返回的http状态码都是200,响应结构的状态码、消息均来源于BizError
变量中。BizError
的变量定义方式如下注意,新增的业务错误码建议从20000开始,因为errorx.New(20001, "用户不存在") errorx.NewDefault("用户不存在") // code默认值为ErrCodeDefaultCommon的值,也就是10000
internal
底层可能会定义10000-20000之内的业务错误码,例如校验失败的错误码是ErrCodeValidateFailed
值为10001,通用错误ErrCodeDefaultCommon
值为10000error
,error应该是其他错误的超类,如果非上述两种错误,我们统一用error
捕获,并且返回响应http状态码200,code为默认值ErrCodeDefaultCommon
,也就是10000{ "code": 10000, "data": null, "message": "用户不存在", "trace_id": "dc119c64-d4b9-4af1-9e02-d15fc4ba2e42" }
-
请求第三方接口 接入了
go-resty
库,并做了简单封装,便于开箱即用-
原生方式
resp, err := httpc.POST(ctx, "http://localhost:8080/api/list"). SetFormData(httpc.M{"username": "aaaa", "age": "55555"}). Send()
如上,主要对go-resty进行了简单封装,封装成了
httpc
库,并提供了POST
,GET
常用两种请求方式 -
服务方式
如果第三方接口交互较多,可以作为服务进行对接,首先在
main.go
文件配置第三方服务地址,例如user.Init("http://localhost:8080")
然后在
rest
目录定义服务相关文件主要包括init.go
启动文件response.go
接口返回格式以及解析响应结果svc.go
定义服务接口、参数以及响应结构,进行明确要求,便于代码的可维护性svc.impl.go
对svc.go中接口的实现 定义要上面几个文件之后,便可以在自己的业务文件中发起请求了hash := md5.Sum([]byte("abcd")) pwd := hex.EncodeToString(hash[:]) resp, err := login.Svc.Login(ctx, &login.LoginReq{Username: "1", Pwd: pwd}) if err != nil { httpx.Error(ctx, err) return }
-
-
队列
队列使用的是比较热门的库
github.com/hibiken/asynq
,本项目稍微进行了一点点儿封装,简化使用,更加结构化,便于代码的维护,弱化了client和server端指定taskname- 队列server目录为
cmd/task
- 队列代码维护在
tasks/
目录 - 将数据写入队列的方式,封装了3个方法
task.Dispatch(tasks.NewSampleTask("测试3333"),3*time.Secord) // 使用task包下的Dispatch方法,并添加延迟时间3s后执行 task.DispatchWithRetry(tasks.NewSampleTask("测试3333"),)// 使用task包下的Dispatch方法,并添加延迟时间和失败后的重试次数 task.DispatchNow(tasks.NewSampleTask("测试3333")) // 使用task包下的Dispatch方法,立即执行 tasks.NewSampleTask("测试3333").DispatchNow() // 使用task结构的DispatchNow方法
- server端handler处理,首先需要将没一个task的handler维护到server端,在
tasks/init.go
文件进行添加task.Handle(NewSampleTaskHandler()) // Handle是封装的一个方法
- 队列server目录为
1. git clone [email protected]:fanqingxuan/go-gin.git
2. cd go-gin && go mod tidy
3. web启动方式 go run cmd/api/main.go -f .env
4. 定时任务 go run cmd/cron/main.go -f .env