Gin 前后端分离项目实战从 0 到上线一个多端 GinGorm项目前言最近为了系统练习 Go 后端开发我做了一个完整的多端小项目一间清单 Todo-list。它不是一个只写几个接口的 Demo而是把一个后端项目从功能设计、数据库建模、接口开发、前端联调到最后部署上线完整走了一遍。项目本身不复杂但覆盖了 Gin 入门到实战里非常常见的一套流程。项目核心功能是用户输入一个房间名称进入独立的任务空间。在每个房间中可以分别管理「学习清单」和「任务清单」。项目地址https://github.com/tuoxie423/Todo-List.git在线体验https://list.tuoxie.asia小程序端开发中的微信小程序一间清单为什么做这个项目学习 Gin 的时候如果只写单个接口很容易停留在“会用”的阶段。真正做一个项目时还会遇到很多实际问题路由怎么组织表结构怎么设计前后端怎么联调配置文件怎么管理数据怎么隔离部署后 502 怎么排查服务怎么后台稳定运行所以我选择做一个小而完整的任务清单项目用一个清晰的业务场景把这些知识点串起来。项目功能这个项目主要实现了这些功能输入房间名称创建或进入房间每个房间拥有独立任务数据管理学习清单管理任务清单新增任务标记完成或恢复未完成删除任务浏览器端记录最近进入过的房间网站端和小程序端共用同一套后端 API整体可以理解为一个轻量级的多端任务清单系统。项目结构项目目录大致如下. ├── backend │ ├── main.go │ ├── config │ │ └── config.go │ ├── global │ │ └── global.go │ └── model │ ├── room.go │ └── task.go ├── frontend │ ├── home.html │ ├── home.js │ ├── tasks.html │ ├── app.js │ └── server.js ├── miniprogram │ ├── pages │ └── utils ├── config │ └── config.example.yaml └── README.md其中backendGin 后端服务frontend浏览器端页面miniprogram微信小程序端config统一管理端口、数据库等配置数据库模型设计项目里有两个核心模型房间和任务。房间表房间用于区分不同用户输入的任务空间。typeRoomstruct{IDintgorm:primaryKey json:idNamestringgorm:type:varchar(100);not null;uniqueIndex json:nameCreatedAt time.Timejson:createdAt}对应数据库表可以理解为rooms --- id 主键 name 房间名称唯一 created_at 创建时间这里给name加了唯一索引。这样用户再次输入同一个房间名时会进入原来的清单而不是创建一个重复房间。任务表任务表通过room_id和房间关联。typeTaskstruct{IDintgorm:primaryKey json:idRoomIDintgorm:not null;index json:roomIdTitlestringgorm:type:varchar(100);not null json:titleLevelstringgorm:type:varchar(20);not null;default:基础 json:levelKindstringgorm:type:varchar(30);not null;default:learning json:kindDoneboolgorm:not null;default:false json:doneCreatedAt time.Timejson:createdAt}对应数据库表可以理解为tasks --- id 主键 room_id 所属房间 ID title 任务标题 level 任务难度 kind 任务类型learning / optimization done 是否完成 created_at 创建时间这样设计之后查询任务时必须带上room_id可以保证不同房间之间的数据互不影响。接口设计后端接口按照 RESTful 风格设计POST /api/rooms GET /api/rooms/:roomID/tasks POST /api/rooms/:roomID/tasks PATCH /api/rooms/:roomID/tasks/:taskID/toggle DELETE /api/rooms/:roomID/tasks/:taskID创建或进入房间POST /api/rooms请求体{name:Go 后端练习}后端会先根据房间名查询。如果房间不存在就创建如果已经存在就直接返回已有房间。查询房间任务GET /api/rooms/:roomID/tasks返回{items:[{id:1,roomId:1,title:跑通 Gin 后端服务,level:基础,kind:learning,done:false,createdAt:2026-06-27T...}]}新增任务POST /api/rooms/:roomID/tasks请求体{title:学习 GORM 表关联,level:进阶,kind:learning}其中kind用来区分不同清单learning 学习清单 optimization 任务清单Gin 路由实现后端使用 Gin 创建路由funcsetupRouter(db*gorm.DB)*gin.Engine{router:gin.New()router.Use(gin.Logger(),gin.Recovery(),corsMiddleware())router.GET(/health,func(c*gin.Context){c.JSON(http.StatusOK,gin.H{status:ok,time:time.Now().Format(time.RFC3339),})})api:router.Group(/api){api.POST(/rooms,func(c*gin.Context){// 创建或进入房间})roomTasks:api.Group(/rooms/:roomID/tasks){roomTasks.GET(,func(c*gin.Context){// 查询任务})roomTasks.POST(,func(c*gin.Context){// 创建任务})roomTasks.PATCH(/:taskID/toggle,func(c*gin.Context){// 切换任务状态})roomTasks.DELETE(/:taskID,func(c*gin.Context){// 删除任务})}}returnrouter}这里把任务接口都放在/api/rooms/:roomID/tasks下面是因为任务必须属于某一个房间。这样接口语义更清楚也能避免误操作其他房间的数据。参数校验项目里对房间 ID 和任务 ID 做了统一校验funcparseParamID(c*gin.Context,namestring,messagestring)(int,bool){id,err:strconv.Atoi(c.Param(name))iferr!nil||id0{c.JSON(http.StatusBadRequest,gin.H{message:message})return0,false}returnid,true}使用时很直接roomID,ok:parseParamID(c,roomID,房间 ID 必须是正整数)if!ok{return}除了 ID 校验还要判断房间是否存在funcroomExists(c*gin.Context,db*gorm.DB,roomIDint)bool{varcountint64iferr:db.Model(model.Room{}).Where(id ?,roomID).Count(count).Error;err!nil{c.JSON(http.StatusInternalServerError,gin.H{message:查询房间失败})returnfalse}ifcount0{c.JSON(http.StatusNotFound,gin.H{message:房间不存在})returnfalse}returntrue}这样可以避免给不存在的房间添加任务。创建任务流程创建任务时后端主要做了这些事解析roomID判断房间是否存在解析 JSON 请求体校验任务标题不能为空规范化难度和任务类型写入数据库返回创建后的任务数据核心代码varpayload createTaskRequestiferr:c.ShouldBindJSON(payload);err!nil{c.JSON(http.StatusBadRequest,gin.H{message:请求体必须是 JSON})return}title:strings.TrimSpace(payload.Title)iftitle{c.JSON(http.StatusBadRequest,gin.H{message:任务标题不能为空})return}task:model.Task{RoomID:roomID,Title:title,Level:normalizeLevel(payload.Level),Kind:normalizeKind(payload.Kind),}iferr:db.Create(task).Error;err!nil{c.JSON(http.StatusInternalServerError,gin.H{message:创建任务失败})return}c.JSON(http.StatusCreated,task)这里前端虽然也会做表单校验但后端仍然必须校验。因为后端不能完全相信客户端传来的数据。前端如何调用接口前端通过fetch调用后端接口。例如进入房间constroomawaitrequest(/api/rooms,{method:POST,body:JSON.stringify({name}),});新增任务consttaskawaitrequest(/api/rooms/${currentRoom.id}/tasks,{method:POST,body:JSON.stringify({title,level:levelInput.value,kind,}),});前端页面本身不直接操作数据库只负责收集用户输入然后调用后端接口。真正的数据保存逻辑都在后端完成。多端适配这个项目除了浏览器端还保留了微信小程序端目录。小程序端和网站端共用同一套 API只是请求方式不同浏览器端fetch 小程序端wx.request小程序「一间清单」目前仍在开发/发布中。等后端接口和网站稳定后小程序只需要把接口域名配置为https://list.tuoxie.asia并在微信公众平台配置 request 合法域名即可。配置文件管理项目使用 YAML 文件统一管理配置。示例backend:port:18080mode:releasefrontend:port:18090apiBase:http://localhost:18080database:host:localhostport:3306user:your_usernamepassword:your_passwordname:listcharset:utf8mb4真实配置文件不应该上传仓库只上传示例文件config/config.example.yaml真实文件config/config.yaml应该加入.gitignore避免数据库账号密码泄露。跨域处理前后端分离时浏览器请求后端接口可能遇到跨域问题所以后端加了一个简单的 CORS 中间件funccorsMiddleware()gin.HandlerFunc{returnfunc(c*gin.Context){c.Writer.Header().Set(Access-Control-Allow-Origin,*)c.Writer.Header().Set(Access-Control-Allow-Methods,GET,POST,PATCH,DELETE,OPTIONS)c.Writer.Header().Set(Access-Control-Allow-Headers,Content-Type)ifc.Request.Methodhttp.MethodOptions{c.AbortWithStatus(http.StatusNoContent)return}c.Next()}}练习项目这样写比较简单。正式项目可以根据具体域名收紧允许的来源。部署上线项目上线时我使用 Nginx 对外提供 HTTPS再反向代理到本机前后端服务https://list.tuoxie.asia ↓ Nginx 443 ↓ / - 127.0.0.1:18090 /api/ - 127.0.0.1:18080 /health - 127.0.0.1:18080后端编译命令go build-buildvcsfalse-otodo-list.服务后台运行建议使用systemd这样进程异常退出后可以自动重启也方便查看日志。项目总结这个项目虽然功能简单但覆盖了 Go 后端开发中很常见的一套流程Gin 路由分组RESTful API 设计JSON 参数解析参数校验GORM 操作数据库一对多表关联前后端分离调用YAML 配置管理网站部署上线小程序多端适配对初学者来说它比单独写几个接口更有练习价值。因为它不是只关注某一个知识点而是把一个后端项目从开发到上线的链路完整走了一遍。后续还可以继续扩展增加用户登录增加房间权限增加任务编辑增加分页查询增加接口日志增加更完整的单元测试如果你也在学习 Gin可以把这个项目当作一个练手模板先跑通再理解接口设计最后尝试自己扩展功能。