Go: Go 项目架构设计

最后更新:2026-08-26

Go 没有框架强制的项目结构——但这意味着你需要自己设计。合理的架构让你的项目在 1000 行时清晰,在 10 万行时仍然可控。

当你接手一个把所有代码都堆在 main.go 里的项目时,重构的第一步就是把代码放进正确的目录。

1. 你将学到


2. 一个后端工程师的真实故事

(1) 痛点:10 万行代码全在 main.go

Alice 接手了一个电商项目:

"前任把所有代码都写在 main.go 里:路由注册、数据库操作、HTML template、业务逻辑——混在一起 3 万行。我想加一个用户状态字段,改了 5 个地方才改全。加功能两周,找代码三天。"

GO
// 坏代码:全部堆在 main.go
package main

var db *sql.DB

func main() {
    // 数据库连接
    // 路由注册
    // HTML 模板
    // 用户处理函数
    // 订单处理函数
    // 商品处理函数
    // 全部混在一起!
}

// 找"用户"相关的代码:用 Ctrl+F 搜索 "user",散落在 50 个地方

(2) Go 的解法:4 层架构

TEXT 📖 仅展示
myapp/
├── cmd/
│   └── server/
│       └── main.go         # 入口 → 依赖注入 + 启动服务
├── internal/
│   ├── handler/            # 层 1:HTTP 处理器(解析请求/返回响应)
│   ├── service/            # 层 2:业务逻辑(领域规则)
│   └── repository/         # 层 3:数据访问(数据库/外部 API)
├── pkg/
│   └── model/              # 层 4:领域模型(数据结构)
└── go.mod

(3) 收益:混沌 vs 分层

场景 混沌架构 4 层架构
新增字段 改 5 个未知位置 仅改 model + handler
切换数据库 改所有 db 调用 只改 repository 层
单元测试 无法单元测试 每层可独立 mock 测试
新人上手 翻 3 万行 main.go 看目录名就懂职责

3. 标准项目布局

(1) 目录结构详解

TEXT 📖 仅展示
myproject/
├── cmd/                    # 可执行文件入口
│   ├── server/             #   server 二进制
│   │   └── main.go
│   └── migrate/            #   数据库迁移工具
│       └── main.go
├── internal/               # 私有包(外部不可导入)
│   ├── handler/            #   HTTP 处理器
│   ├── service/            #   业务逻辑
│   └── repository/         #   数据访问
├── pkg/                    # 可导出的公共包
│   └── model/              #   领域模型
├── migrations/             # SQL 迁移文件
├── config/                 # 配置文件
├── scripts/                # 辅助脚本
├── go.mod
└── go.sum

(2) cmd/ / internal/ / pkg/ 职责

目录 用途 外部可导入
cmd/ 可执行入口(main 函数) N/A(是 main 包)
internal/ 私有实现 ❌ Go 编译器禁止外部导入
pkg/ 对外暴露的公共代码
💡 提示: internal 目录是 Go 编译器的特殊目录——任何在 internal 上级目录之外的包都无法导入它。这提供了真正的封装,比"命名约定"(如 _private)更安全。


4. Clean Architecture 4 层模型

TEXT 📖 仅展示
handler (HTTP) → service (业务) → repository (数据)
     ↓                ↓                ↓
   请求解析         领域规则         数据库/API
   响应返回         事务管理         CRUD 操作
   参数验证         多步骤编排        缓存访问

▶ 示例:4 层实现

GO 📖 仅展示
// ---------- 层 4:Model(领域模型) ----------
// pkg/model/user.go
package model

import "time"

type User struct {
    ID        int
    Name      string
    Email     string
    CreatedAt time.Time
}

type CreateUserRequest struct {
    Name  string
    Email string
}

// ---------- 层 3:Repository(数据访问) ----------
// internal/repository/user.go
package repository

import (
    "database/sql"
    "myapp/pkg/model"
)

type UserRepository interface {
    FindByID(id int) (*model.User, error)
    Create(req model.CreateUserRequest) (*model.User, error)
    List() ([]*model.User, error)
    Delete(id int) error
}

type userRepository struct {
    db *sql.DB
}

func NewUserRepository(db *sql.DB) UserRepository {
    return &userRepository{db: db}
}

func (r *userRepository) FindByID(id int) (*model.User, error) {
    row := r.db.QueryRow("SELECT id, name, email, created_at FROM users WHERE id = ?", id)
    user := &model.User{}
    err := row.Scan(&user.ID, &user.Name, &user.Email, &user.CreatedAt)
    if err == sql.ErrNoRows {
        return nil, nil
    }
    return user, err
}

func (r *userRepository) Create(req model.CreateUserRequest) (*model.User, error) {
    result, err := r.db.Exec("INSERT INTO users (name, email) VALUES (?, ?)", req.Name, req.Email)
    if err != nil {
        return nil, err
    }
    id, _ := result.LastInsertId()
    return r.FindByID(int(id))
}

// ---------- 层 2:Service(业务逻辑) ----------
// internal/service/user.go
package service

import (
    "errors"
    "myapp/internal/repository"
    "myapp/pkg/model"
    "strings"
)

var (
    ErrUserNotFound    = errors.New("user not found")
    ErrInvalidName     = errors.New("name is required")
    ErrInvalidEmail    = errors.New("invalid email format")
)

type UserService struct {
    repo repository.UserRepository
}

func NewUserService(repo repository.UserRepository) *UserService {
    return &UserService{repo: repo}
}

func (s *UserService) Create(req model.CreateUserRequest) (*model.User, error) {
    if strings.TrimSpace(req.Name) == "" {
        return nil, ErrInvalidName
    }
    if !strings.Contains(req.Email, "@") {
        return nil, ErrInvalidEmail
    }
    return s.repo.Create(req)
}

func (s *UserService) Get(id int) (*model.User, error) {
    user, err := s.repo.FindByID(id)
    if err != nil {
        return nil, err
    }
    if user == nil {
        return nil, ErrUserNotFound
    }
    return user, nil
}

// ---------- 层 1:Handler(HTTP 处理器) ----------
// internal/handler/user.go
package handler

import (
    "encoding/json"
    "errors"
    "net/http"
    "strconv"

    "myapp/internal/service"
    "myapp/pkg/model"
)

type UserHandler struct {
    svc *service.UserService
}

func NewUserHandler(svc *service.UserService) *UserHandler {
    return &UserHandler{svc: svc}
}

func (h *UserHandler) CreateUser(w http.ResponseWriter, r *http.Request) {
    var req model.CreateUserRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        writeError(w, http.StatusBadRequest, "invalid JSON")
        return
    }

    user, err := h.svc.Create(req)
    if err != nil {
        writeError(w, http.StatusBadRequest, err.Error())
        return
    }

    writeJSON(w, http.StatusCreated, user)
}

func (h *UserHandler) GetUser(w http.ResponseWriter, r *http.Request) {
    id, _ := strconv.Atoi(r.PathValue("id"))

    user, err := h.svc.Get(id)
    if errors.Is(err, service.ErrUserNotFound) {
        writeError(w, http.StatusNotFound, err.Error())
        return
    }
    if err != nil {
        writeError(w, http.StatusInternalServerError, "internal error")
        return
    }

    writeJSON(w, http.StatusOK, user)
}

// ---------- 工具函数 ----------

func writeJSON(w http.ResponseWriter, status int, data interface{}) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    json.NewEncoder(w).Encode(data)
}

func writeError(w http.ResponseWriter, status int, msg string) {
    writeJSON(w, status, map[string]string{"error": msg})
}
逻辑代码 132 行(超过 40 行限制,仅展示)

(1) 依赖方向

TEXT 📖 仅展示
Handler → Service → Repository (→ DB)
   |          |           |
   ↓          ↓           ↓
  Model     Model       Model
职责 dependency 可测试
Handler HTTP 解析/response Service mock Service
Service 业务规则/编排 Repository mock Repository
Repository 数据 CRUD database/sql mock DB / 集成测试
Model 数据结构
🔥 易错: 依赖方向必须从外向内。 Handler 知道 Service,Service 知道 Repository,但 Repository 不能知道 Service。每一层只依赖它的下层,通过接口解耦(Go 的隐式接口让这一点很自然)。


5. 依赖注入

▶ 示例:构造函数注入

GO
// cmd/server/main.go
package main

import (
    "database/sql"
    "log"
    "net/http"

    "myapp/internal/handler"
    "myapp/internal/repository"
    "myapp/internal/service"
)

func main() {
    // ---- 依赖注入(组装所有层) ----

    // 1. 数据库连接
    db, err := sql.Open("sqlite3", "./app.db")
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()

    // 2. Repository 层
    userRepo := repository.NewUserRepository(db)
    orderRepo := repository.NewOrderRepository(db)

    // 3. Service 层(依赖 Repository)
    userSvc := service.NewUserService(userRepo)
    orderSvc := service.NewOrderService(orderRepo, userRepo)

    // 4. Handler 层(依赖 Service)
    userHandler := handler.NewUserHandler(userSvc)
    orderHandler := handler.NewOrderHandler(orderSvc)

    // 5. 路由注册
    mux := http.NewServeMux()
    userHandler.Register(mux, "/api/v1/users")
    orderHandler.Register(mux, "/api/v1/orders")

    log.Println("服务启动于 :8080")
    log.Fatal(http.ListenAndServe(":8080", mux))
}
▶ 试一试

(2) 依赖注入方式对比

方式 实现 优点 缺点
构造函数注入 NewXxx(dep) 显式、编译期检查 依赖多时代码长
手动 wire main 函数组装 无需第三方库 大项目维护繁琐
Google Wire 代码生成 自动注入 学习成本
💡 提示: 小项目手动注入就够了。当你的项目有 20+ 个 service 和 50+ 个依赖时,考虑 Google Wire(github.com/google/wire)自动生成依赖注入代码。


6. 完整示例:电商项目骨架

⚙️ 前置安装:运行 go get github.com/mattn/go-sqlite3 ⚠️ 注意:go-sqlite3 需要 CGO,Windows 需安装 gcc(MinGW-w64),macOS/Linux 自带

GO
// cmd/server/main.go
package main

import (
    "context"
    "database/sql"
    "encoding/json"
    "log"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"

    _ "github.com/mattn/go-sqlite3"
)

// ---------- Model (pkg/model) ----------

type Product struct {
    ID    int     `json:"id"`
    Name  string  `json:"name"`
    Price float64 `json:"price"`
}

type Order struct {
    ID        int       `json:"id"`
    UserID    int       `json:"user_id"`
    ProductID int       `json:"product_id"`
    Quantity  int       `json:"quantity"`
    Total     float64   `json:"total"`
    Status    string    `json:"status"`
    CreatedAt time.Time `json:"created_at"`
}

// ---------- Repository (internal/repository) ----------

type ProductRepository struct {
    db *sql.DB
}

func NewProductRepository(db *sql.DB) *ProductRepository {
    return &ProductRepository{db: db}
}

func (r *ProductRepository) FindByID(id int) (*Product, error) {
    p := &Product{}
    err := r.db.QueryRow("SELECT id, name, price FROM products WHERE id = ?", id).
        Scan(&p.ID, &p.Name, &p.Price)
    if err == sql.ErrNoRows {
        return nil, nil
    }
    return p, err
}

func (r *ProductRepository) List() ([]*Product, error) {
    rows, err := r.db.Query("SELECT id, name, price FROM products")
    if err != nil {
        return nil, err
    }
    defer rows.Close()

    var products []*Product
    for rows.Next() {
        p := &Product{}
        if err := rows.Scan(&p.ID, &p.Name, &p.Price); err != nil {
            return nil, err
        }
        products = append(products, p)
    }
    return products, rows.Err()
}

type OrderRepository struct {
    db *sql.DB
}

func NewOrderRepository(db *sql.DB) *OrderRepository {
    return &OrderRepository{db: db}
}

func (r *OrderRepository) Create(order *Order) error {
    result, err := r.db.Exec(
        "INSERT INTO orders (user_id, product_id, quantity, total, status) VALUES (?, ?, ?, ?, ?)",
        order.UserID, order.ProductID, order.Quantity, order.Total, order.Status,
    )
    if err != nil {
        return err
    }
    id, _ := result.LastInsertId()
    order.ID = int(id)
    return nil
}

func (r *OrderRepository) FindByID(id int) (*Order, error) {
    o := &Order{}
    err := r.db.QueryRow(
        "SELECT id, user_id, product_id, quantity, total, status, created_at FROM orders WHERE id = ?", id,
    ).Scan(&o.ID, &o.UserID, &o.ProductID, &o.Quantity, &o.Total, &o.Status, &o.CreatedAt)
    if err == sql.ErrNoRows {
        return nil, nil
    }
    return o, err
}

// ---------- Service (internal/service) ----------

type OrderService struct {
    productRepo *ProductRepository
    orderRepo   *OrderRepository
}

func NewOrderService(productRepo *ProductRepository, orderRepo *OrderRepository) *OrderService {
    return &OrderService{
        productRepo: productRepo,
        orderRepo:   orderRepo,
    }
}

type PlaceOrderInput struct {
    UserID    int
    ProductID int
    Quantity  int
}

var (
    ErrProductNotFound = &AppError{Code: "PRODUCT_NOT_FOUND", Message: "商品不存在", HTTPStatus: 404}
    ErrInsufficientStock = &AppError{Code: "INSUFFICIENT_STOCK", Message: "库存不足", HTTPStatus: 409}
)

type AppError struct {
    Code       string `json:"code"`
    Message    string `json:"message"`
    HTTPStatus int    `json:"-"`
}

func (e *AppError) Error() string {
    return e.Message
}

func (s *OrderService) PlaceOrder(input PlaceOrderInput) (*Order, error) {
    product, err := s.productRepo.FindByID(input.ProductID)
    if err != nil {
        return nil, err
    }
    if product == nil {
        return nil, ErrProductNotFound
    }

    total := product.Price * float64(input.Quantity)

    order := &Order{
        UserID:    input.UserID,
        ProductID: input.ProductID,
        Quantity:  input.Quantity,
        Total:     total,
        Status:    "created",
    }

    if err := s.orderRepo.Create(order); err != nil {
        return nil, err
    }

    return order, nil
}

// ---------- Handler (internal/handler) ----------

type OrderHandler struct {
    svc *OrderService
}

func NewOrderHandler(svc *OrderService) *OrderHandler {
    return &OrderHandler{svc: svc}
}

func (h *OrderHandler) Register(mux *http.ServeMux, basePath string) {
    mux.HandleFunc("POST "+basePath, h.PlaceOrder)
    mux.HandleFunc("GET "+basePath+"/{id}", h.GetOrder)
}

type placeOrderRequest struct {
    ProductID int `json:"product_id"`
    Quantity  int `json:"quantity"`
}

func (h *OrderHandler) PlaceOrder(w http.ResponseWriter, r *http.Request) {
    // 从 Context 获取 user ID(认证中间件注入)
    userID, ok := r.Context().Value("user_id").(int)
    if !ok {
        writeError(w, http.StatusUnauthorized, "unauthorized")
        return
    }

    var req placeOrderRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        writeError(w, http.StatusBadRequest, "invalid JSON")
        return
    }

    order, err := h.svc.PlaceOrder(PlaceOrderInput{
        UserID:    userID,
        ProductID: req.ProductID,
        Quantity:  req.Quantity,
    })

    if appErr, ok := err.(*AppError); ok {
        writeError(w, appErr.HTTPStatus, appErr.Message)
        return
    }
    if err != nil {
        writeError(w, http.StatusInternalServerError, "internal error")
        return
    }

    writeJSON(w, http.StatusCreated, order)
}

func (h *OrderHandler) GetOrder(w http.ResponseWriter, r *http.Request) {
    // ... 实现
}

func writeJSON(w http.ResponseWriter, status int, data interface{}) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    json.NewEncoder(w).Encode(data)
}

func writeError(w http.ResponseWriter, status int, msg string) {
    writeJSON(w, status, map[string]string{"error": msg})
}

// ---------- Main(依赖注入入口) ----------

func main() {
    db, err := sql.Open("sqlite3", "./shop.db")
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()

    // 初始化表
    db.Exec(`CREATE TABLE IF NOT EXISTS products (
        id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, price REAL
    )`)
    db.Exec(`CREATE TABLE IF NOT EXISTS orders (
        id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER,
        product_id INTEGER, quantity INTEGER, total REAL,
        status TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP
    )`)

    // 依赖注入
    productRepo := NewProductRepository(db)
    orderRepo := NewOrderRepository(db)
    orderSvc := NewOrderService(productRepo, orderRepo)
    orderHandler := NewOrderHandler(orderSvc)

    mux := http.NewServeMux()
    orderHandler.Register(mux, "/api/v1/orders")

    // 健康检查
    mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
        writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
    })

    server := &http.Server{
        Addr:    ":8080",
        Handler: mux,
    }

    // 优雅关闭
    go func() {
        sigCh := make(chan os.Signal, 1)
        signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
        <-sigCh
        log.Println("正在关闭服务...")
        ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
        defer cancel()
        server.Shutdown(ctx)
    }()

    log.Println("电商服务启动于 :8080")
    if err := server.ListenAndServe(); err != http.ErrServerClosed {
        log.Fatal(err)
    }
}
100%
graph TB
    subgraph Handler
        H[HTTP Handler]
    end
    subgraph Service
        S[Business Logic]
    end
    subgraph Repository
        R[Data Access]
    end
    subgraph Model
        M[Domain Structs]
    end
    subgraph External ["外部依赖"]
        DB[(Database)]
    end

    H -->|调用| S
    S -->|调用| R
    R -->|查询| DB
    H --- M
    S --- M
    R --- M

    style Handler fill:#e1f5fe
    style Service fill:#fff3e0
    style Repository fill:#e8f5e9
    style Model fill:#f3e5f5
🔥 易错: 不要跳过任何一层。 即使你的 Service 只是调用了 Repository 的一个方法,也要走 Service 层——业务规则未来会增长。跳过层会导致业务逻辑散落在 Handler 中,无法单元测试。


❓ 常见问题

Q Clean Architecture 是什么?
A 一种分层架构模式——从外到内:Handler(接口层)→ Service(用例层)→ Repository(数据层)→ Model(领域层)。核心原则:依赖方向从外向内,内层不知道外层存在。外层实现接口,内层定义接口。
Q 4 层模型怎么分层?
A (1) Handler:HTTP 解析/response、参数验证;(2) Service:业务规则、多步骤编排、transaction;(3) Repository:database CRUD、缓存访问、外部 API;(4) Model:数据结构定义。每层只依赖下一层,通过接口解耦。
Q 依赖注入怎么实现?
A 构造函数注入是最常用的方式:NewXxx(dep1, dep2) *Xxx。在 main function(或 wire 生成代码)中创建所有依赖并组装。优点:(1) 依赖关系清晰;(2) 编译期检查;(3) 单元测试时传递 mock。
Q 为什么用 internal 目录?
A Go 编译器强制 internal 目录下的包只能在父级目录中的代码导入。这提供了真正的封装——外部用户无法导入 internal package。在大型项目中,这防止了架构违规(如 handlers 直接导入 repository)。
Q 如何设计错误类型?
A 定义自定义错误类型,包含 Code/Message/HTTPStatus field。Service 层返回业务错误(如 ErrProductNotFound),Handler 层将错误映射为 HTTP 状态码和 JSON response。好处:统一错误格式,错误类型一目了然。
Q 层之间必须用接口吗?
A 推荐。Go 的隐式接口让依赖反转很自然——Repository 定义接口,Handler 依赖接口,具体实现在构造函数注入。好处:(1) 单元测试可以 mock;(2) 切换实现(如 SQLite→MySQL)无需修改调用方。
Q 小项目也需要 4 层吗?
A 不需要。项目规模决定架构深度——500 行以下用 flat 结构,500-5000 行用 2 层(handler + repository),5000+ 行用 4 层。建议从 2 层开始,随着项目增长逐步分层。不要过度工程化。

📖 小节


📝 作业

  1. 基础题(难度⭐):创建一个项目骨架,包含 cmd/server/main.go、internal/handler/、internal/service/、internal/repository/、pkg/model/ 目录。实现一个简单的 HealthHandler 返回 {"status": "ok"}

  2. 进阶题(难度⭐⭐):按 4 层架构实现商品分类管理。要求:(1) Model: Category(id, name, parent_id);(2) Handler: CRUD endpoints;(3) Service: 验证分类名称唯一、禁止循环引用;(4) Repository: SQLite 实现;(5) main 函数通过构造函数注入组装。

  3. 挑战题(难度⭐⭐⭐):将 lesson 22 的图书管理系统重构为 4 层架构。要求:(1) 分拆到 cmd/internal/pkg 目录;(2) Handler 层只负责 HTTP (3) Service 层包含完整业务验证;(4) Repository 层用 SQLite;(5) 接口化 Repository 使 mock 测试可行;(6) 写单元测试(mock repository)测试 Service 层的业务规则。

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏