gin.default()不适合任务看板后端,因其默认启用logger(高并发刷屏干扰调试)和recovery(掩盖panic真实原因,如连接池耗尽),应改用gin.new()手动装配:dev环境用loggerwithwriter定向输出,自定义recovery上报错误并触发重试,websocket场景须禁用默认recovery以防连接中断。

为什么不用 gin.Default() 启动任务看板后端
gin.Default() 自带 Logger 和 Recovery 中间件,开发阶段看着省事,但任务看板这类系统常需对接定时任务、WebSocket 实时更新、或与前端低频长连接交互——默认日志会刷屏干扰调试,崩溃恢复可能掩盖真实 panic(比如数据库连接池耗尽、任务队列阻塞),反而让问题更难定位。
建议改用 gin.New() 手动装配:
- 只在 dev 环境加
gin.LoggerWithWriter()输出到文件或特定 io.Writer,避免 stdout 混乱 - 用自定义
Recovery中间件捕获 panic 后上报 Sentry 或写入本地 error.log,并主动触发任务重试机制 - 若启用 WebSocket(如看板实时拖拽同步),必须禁用默认 Recovery,否则 panic 会中断连接且不通知前端
c.BindJSON() 解析任务结构体时字段总为空
常见现象:前端 POST {"title":"修复登录bug","status":"todo"},后端 struct{Title string `json:"title"`} 却读到空字符串。
根本原因不是标签写错,而是 Gin 默认不校验 JSON 字段是否存在,且对零值(空字符串、0、nil)不做拦截。
- 必须显式加
binding:"required"标签,例如Title string `json:"title" binding:"required"` - 若字段允许为空但需区分“未传”和“传了空值”,用指针类型:
Title *string `json:"title"`,然后判断task.Title == nil - 别依赖
c.ShouldBindJSON()的返回值就认为数据安全——它只检查语法和类型,不校验业务逻辑(比如 status 是否在 todo/in-progress/done 范围内)
路由分组后 r.Group("/api/v1/tasks") 的中间件没生效
典型错误:写了 tasks := r.Group("/api/v1/tasks"); tasks.Use(AuthMiddleware),但所有 GET /api/v1/tasks 请求都没进中间件。
问题出在注册顺序:Gin 的中间件只对 Group 内后续注册的路由生效,且必须在 Use() 之后调用 .GET()、.POST() 等方法。
- 正确写法:
tasks := r.Group("/api/v1/tasks"); tasks.Use(AuthMiddleware); tasks.GET("", ListTasksHandler) - 如果中间件要跨多个 Group(比如统一鉴权),直接挂到根引擎:
r.Use(AuthMiddleware),避免漏掉 /health 或 /ws 路由 - 任务看板常需权限分级(管理员可删任务、成员只能改状态),建议用
c.GetString("role")从 JWT token 中取角色,而不是在每个 handler 里重复解析
并发更新任务状态时出现脏写,UPDATE tasks SET status=? WHERE id=? 不够用
多人同时拖拽同一任务到不同列,后端收到两个请求,都查到旧状态,都执行 UPDATE,最终状态取决于谁 commit 更晚——这不是 Gin 的问题,但 Gin 的无状态设计会让这个问题更隐蔽。
必须在数据库层加约束,不能只靠应用逻辑。
- 给 status 字段加检查约束:
ALTER TABLE tasks ADD CONSTRAINT chk_status CHECK (status IN ('todo','in-progress','done')); - UPDATE 语句带上旧状态条件:
UPDATE tasks SET status = ? WHERE id = ? AND status = ?,然后检查rowsAffected == 1,否则返回 409 Conflict - 避免用
SELECT + UPDATE两步操作——即使加了FOR UPDATE,在高并发下仍可能因事务隔离级别导致间隙锁争用











