基于 Gin 框架的 大型 Web 项目推荐架构目录结

最近一直在做客服系统,算法就一个人,没有后台和前端人员支撑,只能自己研究,从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
}
相关推荐
foundbug9995 小时前
Modbus协议C语言实现(易于移植版本)
java·c语言·前端
Luna-player5 小时前
在前端中list.map的用法
前端·数据结构·list
用户47949283569155 小时前
面试官问 React Fiber,这一篇文章就够了
前端·javascript·react.js
小徐_23335 小时前
Gemini 3做粒子交互特效很出圈?拿 TRAE SOLO 来实现一波!
前端·ai编程·trae
LYFlied5 小时前
【一句话概述】Webpack、Vite、Rollup 核心区别
前端·webpack·node.js·rollup·vite·打包·一句话概述
reddingtons5 小时前
PS 参考图像:线稿上色太慢?AI 3秒“喂”出精细厚涂
前端·人工智能·游戏·ui·aigc·游戏策划·游戏美术
一水鉴天5 小时前
整体设计 定稿 之23+ dashboard.html 增加三层次动态记录体系仪表盘 之2 程序 (Q199 之2) (codebuddy)
开发语言·前端·javascript
刘发财5 小时前
前端一行代码生成数千页PDF,dompdf.js新增分页功能
前端·typescript·开源
_请输入用户名5 小时前
Vue 3 源码项目结构详解
前端·vue.js