如果您按照我们提供的已有示例代码进行开发,一般是没有任何问题的,但是如果您有新的想法,想在某个地方改写项目骨架语法,模式等,那么可能或遇到“坑”。
本篇我们将汇总使用过程中最常见的问题, 很多细小的问题或许在这里你能找到答案.

1.为什么该项目 go.mod 中的模块名是 goskeleton ,但是下载下来的文件名却是 GinSkeleton ?

本项目一开始我们命名为 ginskeleton , 包名也是这个,但是后来感觉 goskeleton 好听一点,因此改名(现在看是错了),由于版本已经更新较多,但是不影响使用,此次失误请忽略即可.

2.为什么编译后的文件提示 config.yml 文件不存在 ?

项目的编译仅限于代码部分,不包括资源部分:configpublicstorage资源目录,因此编译后的文件使用时,需要手动将这个三个目录复制到最终编译的可执行文件同目录,否则程序无法正常运行.

3.表单参数验证器代码部分的疑问

示例代码位置:app/http/validator/web/users/register.go ,如下代码段

  1. type Register struct {
  2. Base
  3. Pass string `form:"pass" json:"pass" binding:"required,min=3,max=20"` //必填,密码长度范围:【3,20】闭区间
  4. Phone string `form:"phone" json:"phone" binding:"required,len=11"` // 验证规则:必填,长度必须=11
  5. //CardNo string `form:"card_no" json:"card_no" binding:"required,len=18"` //身份证号码,必填,长度=18
  6. }
  7. // 注意这里绑定在了 Register ,接收器是一个非指针
  8. func (r Register) CheckParams(context *gin.Context) {
  9. // ...
  10. }

CheckParams 函数是否可以绑定在指针上?例如写成如下:

  1. // 注意这里绑定在了 *Register ,接收器是一个指针
  2. func (r *Register) CheckParams(context *gin.Context) {
  3. // ...
  4. }

这里绝对不可以,因为表单参数验证器在程序启动时会自动注册在容器,每次调用都必须是一个全新的初始化代码段,如果绑定在指针,第一次请求验证通过之后,相关的参数值就会绑定容器中的代码上,造成下次请求数据污染.

4.表单参数验证器中的结构体字段数据类型特别注意事项

总体来说,常用的最基本数据类型主要包括 :
string、bool、time.time 、
数字类型(int8、uint8、int16、uint16、int32、uint64、float32、float64),尤其是数字类型在go语言被划分的很细。
在ginskeleton中,time.time 时间类型我们统一使用 stirng 类型的本地时间格式(yyyy-mm-dd H:i:s)处理即可,世界上其他的时间格式还有:GMT(格林威治时间格式) 、UTC(go语言的 time.time 就是这种格式)、timestamp(时间戳),以上格式都是标准时间格式,目前绝大部分系统主要还是以本地时间格式为多。
数字类型我们在参数验证器统一使用 float64 接受,后续的逻辑——控制器、model中可以获取 float64 类型,根据具体的需要再次进行细分转换即可,但是请您记住,在表单参数验证器之后的控制器等,从上下文获取数字参数,必须使用 float64 ,否则使用我们提供的快捷方法获取不到相关键的数字值。

  1. type Update struct {
  2. BaseField // 表单参数验证结构体支持匿名结构体嵌套、以及匿名结构体与普通字段组合
  3. // 数字类型接受时可以用使用 int系列、float64 接受, *float64、*int 表示接受 0
  4. // 虽然接受参数时数字类型可以多样,但是后文如果用我们提供的快捷方式获取数字,必须用 context.Getfloat64() 函数才行
  5. Id float64 `form:"id" json:"id" binding:"required,min=1"`
  6. Age int `form:"age" json:"age" binding:"required,min=1"`
  7. // *int 表示接受的值允许有 0 值
  8. Status *int `form:"status" json:"status" binding:"required,min=0"`
  9. RealName string `form:"real_name" json:"real_name" binding:"required,min=2"`
  10. Phone string `form:"phone" json:"phone" binding:"required,len=11"`
  11. Remark string `form:"remark" json:"remark"`
  12. }
  13. func (u Update) CheckParams(context *gin.Context) {
  14. // ... ...
  15. }

5.控制器结构体的字段注意事项

以原来存在的一个线程安全bug为示例,进行介绍控制器字段可能存在的”坑”

  1. # 我们先看一个之前存在bug的路由,生成验证码id
  2. verifyCode := router.Group("captcha")
  3. verifyCode.GET("/", (&captcha.CaptchaController{}).GenerateId)
  4. # 由于在gin框架,路由与它对应的函数会被注册为 key => []handlers 的形式。
  5. # 下次请求时,gin框架实际上执行的回调函数都是第一次注册的函数地址。
  6. // 以下是控制器定义, 这里将字段定义在结构体中,并且相关的函数使用了 指针接收器,造成了结构体字段变成全局变量,
  7. // 在高并发环境下造成了线程不安全的bug。
  8. type CaptchaController struct {
  9. Id string `json:"id"`
  10. ImgUrl string `json:"img_url"`
  11. Refresh string `json:"refresh"`
  12. Verify string `json:"verify"`
  13. }
  14. // 生成验证码ID
  15. func (c *CaptchaController) GenerateId(context *gin.Context) {
  16. // 设置验证码的数字长度(个数)
  17. var length = variable.ConfigYml.GetInt("Captcha.length")
  18. captchaId := captcha.NewLen(length)
  19. c.Id = captchaId
  20. c.ImgUrl = "/captcha/" + captchaId + ".png"
  21. c.Refresh = c.ImgUrl + "?reload=1"
  22. c.Verify = "/captcha/" + captchaId + "/这里替换为正确的验证码进行验证"
  23. response.Success(context, "验证码信息", c)
  24. }

总结:绑定在控制器结构体上的字段如果被指针函数使用了,那么就变成了全部变量,下次请求时,上次的值还会残留。
如果您的确需要的是全局变量变量,那么可以定义在控制器的结构体成员中,如果您需要的是局部变量,那么就避开本次演示的“坑”。
避免并发安全的方法:结构体字段如果不需要全局变量,就在相关函数内部定义成局部变量即可。

6.全局容器的作用是什么?

  1. 本项目使用容器最多的地方:
  2. app/http/validator/common/register_validator/register_validator.go
  3. 根据key从容器调用:routers/web.go > validatorFactory.Create() 函数 ,就是根据注册时的键从容器获取代码.
  4. 目的:
  5. 1.一个请求(request)到达路由以后,需要进行表单参数的校验,如果是传统的方法,就得import相关的验证器文件包,然后调用包中的函数,进行参数验证, 这种做法会导致路由文件的头部会出现N多的import ....包, 因为你一个接口就得一个验证器。
  6. 在这个项目骨架中,我们将验证器全部注册在容器中,路由文件头部只需要导入一个验证器的包就可以通过key调用对应的value(验证器函数)。
  7. 你可以和别人做的项目对比一下,路由文件的头部 import 部分,看看传统方式导入了是不是N个....
  8. 2.因为验证器在项目启动时,率先注册在了容器(内存),因此调用速度也是超级快,性能极佳.
  9. 3.所有的配置项只要使用一次以后,也会注册在容器,后续调用配置项时,都没有IO开销,性能极佳.

7.每个model都要 create 一次,难道每个 model 都是一次数据库连接吗?

  1. 关系型数据库驱动库其实是根据 config.yml中的配置初始化了一次,因此每种数据库全局只有一个连接,以后每一次都是从同一个驱动指针地址,通过ping() 从底层的连接池获取一个连接。用完也是自动释放的.
  2. 看起来每一个表要初始化一次,主要是为了解决任何一个表可以随意切换到别的数据库连接,解决数据库多源场景。

8.为什么该项目强烈建议应用服务器前置nginx?

  1. 1.nginx处理静态资源,几乎是无敌的,尤其是内存占用方面的管理非常完美.
  2. 2.nginx前置很方便做负载均衡.
  3. 3.nginx access.logerror.log 都是行业通用,可以很方便对接到 elk ,进行后续统计、分析、机器学习、报表展示等等.
  4. 4.gin 框架本身建议生产环境切换 gin release模式,该模式我们进行了二次封装,
  5. //无接口访问日志生成,那么你的接口访问日志就必须要搭配 nginx ,同时该模式我们经过测试对比,性能再度提升 5%

9.本项目骨架引用的包,如何更新至最新版?

8.1 本项目骨架主动引入包全部在 go.mod 文件,如果想自己更新至最新版,非常简单,但是必须注意:该包更新的功能兼容现有版本,如果不兼容,可能会导致封装层app/utils/xxx 出现错误,功能也无法正常使用.

8.2 例如:gormv2 目前在用版本是 v1.20.5, 官方最新版本地址:https://github.com/go-gorm/gorm/tags , 最新版 : v1.20.7

  1. // 1. go.mod 文件修改以下版本号至最新版
  2. gorm.io/gorm v1.20.5 ===>
  3. gorm.io/gorm v1.20.7
  4. // 在goland终端或者 go.mod 同目录执行以下命令即可
  5. go mod tidy