最近一直在做客服系统,算法就一个人,没有后台和前端人员支撑,只能自己研究,从python的fastapi和vue开始,慢慢转移到gin和vuede项目,目前总结如下,是一个基于高内聚、低耦合、模块化、可维护、易迁移 的设计原则,适用于中大型 Go Web 项目。目前使用该结构实现了langgraph的配置参数,提示词,密钥管理,知识库管理,等,发现效果不错,非常适合算法岗的一职多兼的同学使用,完全不需要人和后端和前端,实现算法的研发,以及项目交付。
🏗️ 大型 Gin 项目推荐目录结构
bubble/
├── main.go # 程序入口
├── config/ # 配置文件相关
│ ├── config.yaml # YAML 配置文件(开发/测试/生产)
│ └── setting.go # 配置加载与解析(viper)
│
├── routers/ # 路由层(模块化)
│ ├── routers.go # 总路由入口
│ ├── api_v1.go # v1 版本路由注册
│ ├── middleware/ # 路由中间件
│ │ ├── auth.go
│ │ ├── logger.go
│ │ └── recovery.go
│ └── v1/ # 按版本分组
│ ├── todo.go
│ ├── user.go
│ └── post.go
│
├── controller/ # 控制器层(处理 HTTP 请求)
│ ├── todo.go
│ ├── user.go
│ └── base.go # 公共响应方法
│
├── service/ # 业务逻辑层(核心逻辑)
│ ├── todo_service.go
│ ├── user_service.go
│ └── common.go # 公共业务方法
│
├── model/ # 数据模型层(数据库结构)
│ ├── todo.go
│ ├── user.go
│ └── init.go # 数据库初始化(GORM AutoMigrate)
│
├── repository/ # 数据访问层(DAO)
│ ├── todo_repo.go
│ ├── user_repo.go
│ └── base_repo.go # 通用数据库操作封装
│
├── middleware/ # 全局中间件(可选,也可放在 routers/middleware)
│ └── jwt.go
│
├── utils/ # 工具函数
│ ├── response.go # 统一响应格式封装
│ ├── logger.go # 日志封装(zap)
│ ├── jwt.go # JWT 工具
│ └── validator.go # 参数校验扩展
│
├── pkg/ # 第三方封装或公共库
│ └── gormx/ # GORM 扩展(分页等)
│
├── scripts/ # 脚本(部署、数据库迁移等)
│ ├── deploy.sh
│ └── migrate.sql
│
├── web/ # 前端资源(可选,前后端分离可去掉)
│ ├── static/
│ │ ├── css/
│ │ ├── js/
│ │ └── images/
│ └── templates/
│ ├── index.html
│ └── layout.html
│
├── test/ # 测试文件
│ ├── todo_test.go
│ └── integration/
│
├── log/ # 运行日志输出目录
│
├── go.mod
├── go.sum
└── README.md
🧩 各层职责说明(MVC + Service + Repository 模式)
|---------------|------|-----------------------------|
| 目录 | 职责 | 说明 |
| main.go | 程序入口 | 初始化配置、路由、数据库、启动服务 |
| config/ | 配置管理 | 使用 viper 加载 config.yaml |
| routers/ | 路由分发 | 按版本和模块组织路由,支持嵌套 |
| controller/ | 请求处理 | 接收参数、调用 service、返回响应 |
| service/ | 业务逻辑 | 核心逻辑(事务、校验、流程控制) |
| repository/ | 数据访问 | 与数据库交互(CRUD),屏蔽 DB 细节 |
| model/ | 数据结构 | 定义 GORM 模型 |
| middleware/ | 中间件 | JWT、日志、跨域、限流等 |
| utils/ | 工具函数 | 响应封装、JWT 生成、日志等 |
| pkg/ | 公共包 | 可复用的第三方扩展 |
📦 示例代码片段
1. main.go
// main.go
package main
import (
"bubble/config"
"bubble/routers"
"fmt"
"log"
)
func main() {
// 加载配置
if err := config.Init(); err != nil {
log.Fatalf("config init failed: %v", err)
}
// 初始化数据库
// db.Init()
// 设置路由
r := routers.SetupRouter()
port := config.Conf.Port
if port == "" {
port = "8080"
}
fmt.Printf("Server is running at :%s\n", port)
_ = r.Run(":" + port)
}
2. config/setting.go
// config/setting.go
package config
import (
"github.com/spf13/viper"
)
type Config struct {
AppName string `mapstructure:"app_name"`
Port string `mapstructure:"port"`
Release bool `mapstructure:"release"`
MySQL string `mapstructure:"mysql"`
JWTKey string `mapstructure:"jwt_key"`
}
var Conf = &Config{}
func Init() error {
viper.SetConfigName("config")
viper.SetConfigType("yaml")
viper.AddConfigPath(".")
if err := viper.ReadInConfig(); err != nil {
return err
}
return viper.Unmarshal(Conf)
}
3. routers/v1/todo.go
// routers/v1/todo.go
package v1
import (
"bubble/controller"
"github.com/gin-gonic/gin"
)
func SetupTodoRoutes(rg *gin.RouterGroup) {
todo := rg.Group("/todo")
{
todo.POST("", controller.CreateTodo)
todo.GET("", controller.GetTodoList)
todo.PUT("/:id", controller.UpdateTodo)
todo.DELETE("/:id", controller.DeleteTodo)
}
}
4. controller/todo.go
// controller/todo.go
package controller
import (
"bubble/service"
"github.com/gin-gonic/gin"
"net/http"
)
func CreateTodo(c *gin.Context) {
var req struct {
Title string `json:"title" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if err := service.CreateTodo(req.Title); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed"})
return
}
c.JSON(http.StatusOK, gin.H{"msg": "success"})
}
5. service/todo_service.go
// service/todo_service.go
package service
import (
"bubble/model"
"bubble/repository"
)
func CreateTodo(title string) error {
todo := &model.Todo{Title: title, Status: false}
return repository.CreateTodo(todo)
}
6. repository/todo_repo.go
// repository/todo_repo.go
package repository
import (
"bubble/model"
"gorm.io/gorm"
)
func CreateTodo(todo *model.Todo) error {
return db.Create(todo).Error
}
7. model/todo.go
// model/todo.go
package model
type Todo struct {
ID uint `json:"id" gorm:"primaryKey"`
Title string `json:"title"`
Status bool `json:"status"`
}
✅ 优势总结
|-------------|-------------------------------------------|
| 特性 | 说明 |
| ✅ 分层清晰 | Controller → Service → Repository → Model |
| ✅ 易于测试 | 每层可单独单元测试 |
| ✅ 模块化 | 每个功能模块独立文件,可插拔 |
| ✅ 易迁移 | 整个模块复制即可复用 |
| ✅ 支持多版本 API | v1/, v2/ 路由隔离 |
| ✅ 支持中间件分级 | 全局、版本、模块级中间件 |
| ✅ 配置管理 | viper 支持多环境配置 |
🔚 结语
这个架构适合:
- 中大型项目
- 团队协作开发
- 需要长期维护的系统
- 未来可能拆分为微服务
⚠️ 小项目不必如此复杂,但一旦项目变大,这种结构会让你 少踩90%的坑。
脚本自动创建
package main
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
)
func main() {
// ✅ 你可以自由修改这个变量,目录名就会变
projectName := "myproject" // ← 修改这里即可
fmt.Println("🚀 正在生成 Gin 项目: " + projectName)
// 使用函数动态生成路径
makePath := func(suffix string) string {
return filepath.Join(projectName, suffix)
}
// 所有文件路径与内容映射
files := map[string]string{
// ✅ 使用 makePath 动态生成路径
makePath("go.mod"): `module {{.ProjectName}}
go 1.21
require (
github.com/gin-gonic/gin v1.9.1
github.com/spf13/viper v1.15.0
gorm.io/driver/mysql v1.5.0
gorm.io/gorm v1.25.0
go.uber.org/zap v1.24.0
)
`,
makePath("main.go"): `package main
import (
"{{.ProjectName}}/config"
"{{.ProjectName}}/model"
"{{.ProjectName}}/routers"
"log"
)
func main() {
if err := config.Init(); err != nil {
log.Fatalf("配置初始化失败: %v", err)
}
if err := model.Init(); err != nil {
log.Fatalf("数据库初始化失败: %v", err)
}
r := routers.SetupRouter()
port := config.Conf.Port
log.Printf("服务启动中,端口: %s", port)
if err := r.Run(":" + port); err != nil {
log.Fatalf("服务启动失败: %v", err)
}
}
`,
makePath("config/config.yaml"): `app_name: "{{.ProjectName}}"
port: "8080"
release: false
mysql: "root:123456@tcp(127.0.0.1:3306)/{{.ProjectName}}?charset=utf8mb4&parseTime=True&loc=Local"
jwt_key: "your-secret-key-change-in-production"
`,
makePath("config/setting.go"): `package config
import (
"github.com/spf13/viper"
)
type Config struct {
AppName string ` + "`" + `mapstructure:\"app_name\"` + "`" + `
Port string ` + "`" + `mapstructure:\"port\"` + "`" + `
Release bool ` + "`" + `mapstructure:\"release\"` + "`" + `
MySQL string ` + "`" + `mapstructure:\"mysql\"` + "`" + `
JWTKey string ` + "`" + `mapstructure:\"jwt_key\"` + "`" + `
}
var Conf = &Config{}
func Init() error {
viper.SetConfigName("config")
viper.SetConfigType("yaml")
viper.AddConfigPath(".")
if err := viper.ReadInConfig(); err != nil {
return err
}
return viper.Unmarshal(Conf)
}
`,
makePath("routers/routers.go"): `package routers
import (
"{{.ProjectName}}/config"
"{{.ProjectName}}/controller"
"github.com/gin-gonic/gin"
)
func SetupRouter() *gin.Engine {
if config.Conf.Release {
gin.SetMode(gin.ReleaseMode)
}
r := gin.Default()
r.Static("/static", "./web/static")
r.LoadHTMLGlob("./web/templates/*")
r.GET("/", controller.IndexHandler)
SetupV1Routes(r)
return r
}
`,
makePath("routers/api_v1.go"): `package routers
import "github.com/gin-gonic/gin"
func SetupV1Routes(r *gin.Engine) {
v1 := r.Group("/v1")
{
setupTodoRoutes(v1)
}
}
`,
makePath("routers/v1/todo.go"): `package routers
import (
"{{.ProjectName}}/controller"
"github.com/gin-gonic/gin"
)
func setupTodoRoutes(rg *gin.RouterGroup) {
todo := rg.Group("/todo")
{
todo.POST("", controller.CreateTodo)
todo.GET("", controller.GetTodoList)
todo.PUT("/:id", controller.UpdateTodo)
todo.DELETE("/:id", controller.DeleteTodo)
}
}
`,
makePath("controller/todo.go"): `package controller
import (
"{{.ProjectName}}/service"
"github.com/gin-gonic/gin"
"net/http"
)
type TodoCreateRequest struct {
Title string ` + "`" + `json:"title" binding:"required"` + "`" + `
}
func CreateTodo(c *gin.Context) {
var req TodoCreateRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if err := service.CreateTodo(req.Title); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "创建失败"})
return
}
c.JSON(http.StatusOK, gin.H{"msg": "创建成功"})
}
func GetTodoList(c *gin.Context) {
todos, err := service.GetTodoList()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "查询失败"})
return
}
c.JSON(http.StatusOK, gin.H{"data": todos})
}
func UpdateTodo(c *gin.Context) {
id := c.Param("id")
if err := service.ToggleTodoStatus(id); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "更新失败"})
return
}
c.JSON(http.StatusOK, gin.H{"msg": "更新成功"})
}
func DeleteTodo(c *gin.Context) {
id := c.Param("id")
if err := service.DeleteTodo(id); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "删除失败"})
return
}
c.JSON(http.StatusOK, gin.H{"msg": "删除成功"})
}
func IndexHandler(c *gin.Context) {
c.HTML(200, "index.html", nil)
}
`,
makePath("service/todo_service.go"): `package service
import (
"{{.ProjectName}}/repository"
"strconv"
)
func CreateTodo(title string) error {
return repository.CreateTodo(title)
}
func GetTodoList() ([]map[string]interface{}, error) {
return repository.GetTodoList()
}
func ToggleTodoStatus(id string) error {
idUint, err := strconv.ParseUint(id, 10, 64)
if err != nil {
return err
}
return repository.ToggleTodoStatus(uint(idUint))
}
func DeleteTodo(id string) error {
idUint, err := strconv.ParseUint(id, 10, 64)
if err != nil {
return err
}
return repository.DeleteTodo(uint(idUint))
}
`,
makePath("repository/todo_repo.go"): `package repository
import (
"{{.ProjectName}}/model"
"gorm.io/gorm"
)
var db *gorm.DB
func Init(DB *gorm.DB) {
db = DB
}
func CreateTodo(title string) error {
todo := &model.Todo{Title: title, Status: false}
return db.Create(todo).Error
}
func GetTodoList() ([]map[string]interface{}, error) {
var todos []map[string]interface{}
err := db.Table("todos").Find(&todos).Error
return todos, err
}
func ToggleTodoStatus(id uint) error {
return db.Model(&model.Todo{}).Where("id = ?", id).Update("status", gorm.Expr("NOT status")).Error
}
func DeleteTodo(id uint) error {
return db.Delete(&model.Todo{}, id).Error
}
`,
makePath("model/todo.go"): `package model
type Todo struct {
ID uint ` + "`" + `json:"id" gorm:"primaryKey"` + "`" + `
Title string ` + "`" + `json:"title"` + "`" + `
Status bool ` + "`" + `json:"status"` + "`" + `
}
`,
makePath("model/init.go"): `package model
import (
"{{.ProjectName}}/config"
"{{.ProjectName}}/repository"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
var DB *gorm.DB
func Init() error {
var err error
DB, err = gorm.Open(mysql.Open(config.Conf.MySQL), &gorm.Config{})
if err != nil {
return err
}
if err = DB.AutoMigrate(&Todo{}); err != nil {
return err
}
repository.Init(DB)
return nil
}
`,
makePath("utils/response.go"): `package utils
import "github.com/gin-gonic/gin"
func Success(c *gin.Context, data interface{}) {
c.JSON(200, gin.H{
"code": 200,
"msg": "success",
"data": data,
})
}
func Fail(c *gin.Context, msg string) {
c.JSON(200, gin.H{
"code": 400,
"msg": msg,
"data": nil,
})
}
`,
makePath("web/templates/index.html"): `<html>
<head><title>Todo App</title></head>
<body>
<h1>Welcome to Todo App</h1>
<p>API 服务已启动。</p>
</body>
</html>
`,
makePath("web/static/css/app.css"): `body { font-family: Arial, sans-serif; margin: 40px; }`,
makePath("README.md"): `# {{.ProjectName}} Todo App
基于 Gin 的模块化 Web 项目脚手架。
## 🚀 快速启动
1. 启动 MySQL
2. 创建数据库:CREATE DATABASE {{.ProjectName}};
3. 修改 config/config.yaml 中的数据库配置
4. 运行:
go run main.go
访问: http://localhost:8080
`,
}
// ✅ 创建所有文件
for path, content := range files {
dir := filepath.Dir(path)
if err := os.MkdirAll(dir, 0755); err != nil {
panic(fmt.Sprintf("创建目录失败: %s, 错误: %v", dir, err))
}
// ❌ 当前 content 还包含 {{.ProjectName}} 占位符,需要替换
finalContent := ReplaceProjectName(content, projectName)
if err := ioutil.WriteFile(path, []byte(finalContent), 0644); err != nil {
panic(fmt.Sprintf("写入文件失败: %s, 错误: %v", path, err))
}
fmt.Printf("✅ 创建: %s\n", path)
}
fmt.Println("\n🎉 项目生成完成!")
fmt.Println("👉 进入项目: cd " + projectName)
fmt.Println("👉 整理依赖: go mod tidy")
fmt.Println("👉 启动服务: go run main.go")
}
// ReplaceProjectName 替换模板中的 {{.ProjectName}} 为实际项目名
func ReplaceProjectName(content, projectName string) string {
return ReplaceAll(content, "{{.ProjectName}}", projectName)
}
// ReplaceAll 简单的字符串替换(避免引入 text/template 复杂性)
func ReplaceAll(s, old, new string) string {
for {
if !contains(s, old) {
break
}
s = replaceOnce(s, old, new)
}
return s
}
func contains(s, substr string) bool {
return len(s) >= len(substr) && (s[:len(substr)] == substr || contains(s[1:], substr))
}
func replaceOnce(s, old, new string) string {
for i := 0; i <= len(s)-len(old); i++ {
if s[i:i+len(old)] == old {
return s[:i] + new + s[i+len(old):]
}
}
return s
}