3.10.1 项目架构设计

3.10.1 项目架构设计 #

良好的项目架构是成功项目的基础。本节将详细介绍如何设计一个可维护、可扩展的 Go Web 项目架构,包括目录结构、分层设计、依赖管理等关键要素。

项目概述 #

我们将构建一个功能完整的博客系统,包含以下核心功能:

  • 用户管理:注册、登录、个人资料管理
  • 文章管理:发布、编辑、删除、分类
  • 评论系统:文章评论、回复功能
  • 标签系统:文章标签管理
  • 文件上传:图片和附件上传
  • 权限控制:基于角色的访问控制

技术选型 #

核心框架和库 #

// 主要依赖
require (
    github.com/gin-gonic/gin v1.9.1
    github.com/golang-jwt/jwt/v5 v5.0.0
    gorm.io/gorm v1.25.4
    gorm.io/driver/mysql v1.5.1
    github.com/go-redis/redis/v8 v8.11.5
    github.com/spf13/viper v1.16.0
    github.com/go-playground/validator/v10 v10.15.3
    golang.org/x/crypto v0.12.0
    github.com/google/uuid v1.3.0
    github.com/swaggo/gin-swagger v1.6.0
    go.uber.org/zap v1.25.0
)

技术栈说明 #

  • Web 框架:Gin - 高性能 HTTP 框架
  • ORM:GORM - 功能强大的 Go ORM
  • 数据库:MySQL - 可靠的关系型数据库
  • 缓存:Redis - 高性能内存数据库
  • 认证:JWT - 无状态认证方案
  • 配置:Viper - 灵活的配置管理
  • 日志:Zap - 高性能日志库
  • 验证:Validator - 数据验证库
  • 文档:Swagger - API 文档生成

项目目录结构 #

blog-system/
├── cmd/                    # 应用程序入口
│   └── server/
│       └── main.go
├── internal/               # 私有应用代码
│   ├── config/            # 配置管理
│   │   ├── config.go
│   │   └── database.go
│   ├── handler/           # HTTP处理器
│   │   ├── auth.go
│   │   ├── user.go
│   │   ├── article.go
│   │   └── comment.go
│   ├── service/           # 业务逻辑层
│   │   ├── auth.go
│   │   ├── user.go
│   │   ├── article.go
│   │   └── comment.go
│   ├── repository/        # 数据访问层
│   │   ├── user.go
│   │   ├── article.go
│   │   └── comment.go
│   ├── model/             # 数据模型
│   │   ├── user.go
│   │   ├── article.go
│   │   └── comment.go
│   ├── middleware/        # 中间件
│   │   ├── auth.go
│   │   ├── cors.go
│   │   └── logger.go
│   └── router/            # 路由配置
│       └── router.go
├── pkg/                   # 公共库代码
│   ├── jwt/              # JWT工具
│   ├── logger/           # 日志工具
│   ├── response/         # 响应工具
│   ├── validator/        # 验证工具
│   └── utils/            # 通用工具
├── configs/               # 配置文件
│   ├── config.yaml
│   ├── config.dev.yaml
│   └── config.prod.yaml
├── docs/                  # 文档
│   └── swagger/
├── scripts/               # 脚本文件
│   ├── build.sh
│   └── deploy.sh
├── storage/               # 存储目录
│   ├── logs/
│   └── uploads/
├── go.mod
├── go.sum
├── Dockerfile
├── docker-compose.yml
└── README.md

分层架构设计 #

1. 表现层(Handler Layer) #

负责处理 HTTP 请求和响应,进行参数验证和格式转换。

// internal/handler/base.go
package handler

import (
    "net/http"
    "strconv"

    "github.com/gin-gonic/gin"
    "blog-system/pkg/response"
)

// BaseHandler 基础处理器
type BaseHandler struct{}

// GetUserID 从上下文获取用户ID
func (h *BaseHandler) GetUserID(c *gin.Context) (uint, error) {
    userID, exists := c.Get("user_id")
    if !exists {
        return 0, response.ErrUnauthorized
    }
    return userID.(uint), nil
}

// GetPageParams 获取分页参数
func (h *BaseHandler) GetPageParams(c *gin.Context) (int, int) {
    page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
    pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10"))

    if page < 1 {
        page = 1
    }
    if pageSize < 1 || pageSize > 100 {
        pageSize = 10
    }

    return page, pageSize
}

// BindAndValidate 绑定并验证请求参数
func (h *BaseHandler) BindAndValidate(c *gin.Context, obj interface{}) error {
    if err := c.ShouldBindJSON(obj); err != nil {
        return response.NewError(http.StatusBadRequest, "参数格式错误", err.Error())
    }
    return nil
}

2. 业务逻辑层(Service Layer) #

包含核心业务逻辑,协调各个组件完成业务功能。

// internal/service/base.go
package service

import (
    "context"
    "blog-system/internal/repository"
    "blog-system/pkg/logger"
)

// BaseService 基础服务
type BaseService struct {
    repo   repository.Repository
    logger logger.Logger
    ctx    context.Context
}

// NewBaseService 创建基础服务
func NewBaseService(repo repository.Repository, logger logger.Logger) *BaseService {
    return &BaseService{
        repo:   repo,
        logger: logger,
        ctx:    context.Background(),
    }
}

// WithContext 设置上下文
func (s *BaseService) WithContext(ctx context.Context) *BaseService {
    s.ctx = ctx
    return s
}

3. 数据访问层(Repository Layer) #

负责数据持久化操作,封装数据库访问逻辑。

// internal/repository/base.go
package repository

import (
    "context"
    "gorm.io/gorm"
    "blog-system/pkg/logger"
)

// Repository 仓储接口
type Repository interface {
    UserRepository
    ArticleRepository
    CommentRepository
}

// BaseRepository 基础仓储
type BaseRepository struct {
    db     *gorm.DB
    logger logger.Logger
}

// NewRepository 创建仓储实例
func NewRepository(db *gorm.DB, logger logger.Logger) Repository {
    return &repository{
        BaseRepository: BaseRepository{
            db:     db,
            logger: logger,
        },
    }
}

// repository 仓储实现
type repository struct {
    BaseRepository
}

// WithTx 使用事务
func (r *BaseRepository) WithTx(tx *gorm.DB) Repository {
    return &repository{
        BaseRepository: BaseRepository{
            db:     tx,
            logger: r.logger,
        },
    }
}

// Paginate 分页查询
func (r *BaseRepository) Paginate(page, pageSize int) func(db *gorm.DB) *gorm.DB {
    return func(db *gorm.DB) *gorm.DB {
        offset := (page - 1) * pageSize
        return db.Offset(offset).Limit(pageSize)
    }
}

配置管理 #

配置结构定义 #

// internal/config/config.go
package config

import (
    "fmt"
    "time"

    "github.com/spf13/viper"
)

// Config 应用配置
type Config struct {
    Server   ServerConfig   `mapstructure:"server"`
    Database DatabaseConfig `mapstructure:"database"`
    Redis    RedisConfig    `mapstructure:"redis"`
    JWT      JWTConfig      `mapstructure:"jwt"`
    Upload   UploadConfig   `mapstructure:"upload"`
    Log      LogConfig      `mapstructure:"log"`
}

// ServerConfig 服务器配置
type ServerConfig struct {
    Port         int           `mapstructure:"port"`
    Mode         string        `mapstructure:"mode"`
    ReadTimeout  time.Duration `mapstructure:"read_timeout"`
    WriteTimeout time.Duration `mapstructure:"write_timeout"`
}

// DatabaseConfig 数据库配置
type DatabaseConfig struct {
    Driver          string        `mapstructure:"driver"`
    Host            string        `mapstructure:"host"`
    Port            int           `mapstructure:"port"`
    Username        string        `mapstructure:"username"`
    Password        string        `mapstructure:"password"`
    Database        string        `mapstructure:"database"`
    Charset         string        `mapstructure:"charset"`
    MaxIdleConns    int           `mapstructure:"max_idle_conns"`
    MaxOpenConns    int           `mapstructure:"max_open_conns"`
    ConnMaxLifetime time.Duration `mapstructure:"conn_max_lifetime"`
}

// RedisConfig Redis配置
type RedisConfig struct {
    Host     string `mapstructure:"host"`
    Port     int    `mapstructure:"port"`
    Password string `mapstructure:"password"`
    DB       int    `mapstructure:"db"`
}

// JWTConfig JWT配置
type JWTConfig struct {
    Secret     string        `mapstructure:"secret"`
    Expiration time.Duration `mapstructure:"expiration"`
}

// UploadConfig 上传配置
type UploadConfig struct {
    Path      string   `mapstructure:"path"`
    MaxSize   int64    `mapstructure:"max_size"`
    AllowExts []string `mapstructure:"allow_exts"`
}

// LogConfig 日志配置
type LogConfig struct {
    Level      string `mapstructure:"level"`
    Filename   string `mapstructure:"filename"`
    MaxSize    int    `mapstructure:"max_size"`
    MaxAge     int    `mapstructure:"max_age"`
    MaxBackups int    `mapstructure:"max_backups"`
    Compress   bool   `mapstructure:"compress"`
}

// Load 加载配置
func Load(configPath string) (*Config, error) {
    viper.SetConfigFile(configPath)
    viper.AutomaticEnv()

    if err := viper.ReadInConfig(); err != nil {
        return nil, fmt.Errorf("读取配置文件失败: %w", err)
    }

    var config Config
    if err := viper.Unmarshal(&config); err != nil {
        return nil, fmt.Errorf("解析配置文件失败: %w", err)
    }

    return &config, nil
}

// GetDSN 获取数据库连接字符串
func (c *DatabaseConfig) GetDSN() string {
    return fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=%s&parseTime=True&loc=Local",
        c.Username,
        c.Password,
        c.Host,
        c.Port,
        c.Database,
        c.Charset,
    )
}

配置文件示例 #

# configs/config.yaml
server:
  port: 8080
  mode: debug
  read_timeout: 30s
  write_timeout: 30s

database:
  driver: mysql
  host: localhost
  port: 3306
  username: root
  password: password
  database: blog_system
  charset: utf8mb4
  max_idle_conns: 10
  max_open_conns: 100
  conn_max_lifetime: 1h

redis:
  host: localhost
  port: 6379
  password: ""
  db: 0

jwt:
  secret: your-secret-key
  expiration: 24h

upload:
  path: ./storage/uploads
  max_size: 10485760 # 10MB
  allow_exts: [".jpg", ".jpeg", ".png", ".gif", ".pdf", ".doc", ".docx"]

log:
  level: info
  filename: ./storage/logs/app.log
  max_size: 100
  max_age: 30
  max_backups: 10
  compress: true

依赖注入 #

依赖注入容器 #

// internal/container/container.go
package container

import (
    "blog-system/internal/config"
    "blog-system/internal/handler"
    "blog-system/internal/repository"
    "blog-system/internal/service"
    "blog-system/pkg/logger"
    "gorm.io/gorm"
    "github.com/go-redis/redis/v8"
)

// Container 依赖注入容器
type Container struct {
    Config     *config.Config
    DB         *gorm.DB
    Redis      *redis.Client
    Logger     logger.Logger
    Repository repository.Repository
    Service    *service.Services
    Handler    *handler.Handlers
}

// NewContainer 创建容器
func NewContainer(cfg *config.Config) (*Container, error) {
    container := &Container{
        Config: cfg,
    }

    // 初始化日志
    if err := container.initLogger(); err != nil {
        return nil, err
    }

    // 初始化数据库
    if err := container.initDatabase(); err != nil {
        return nil, err
    }

    // 初始化Redis
    if err := container.initRedis(); err != nil {
        return nil, err
    }

    // 初始化仓储层
    container.initRepository()

    // 初始化服务层
    container.initService()

    // 初始化处理器层
    container.initHandler()

    return container, nil
}

// initLogger 初始化日志
func (c *Container) initLogger() error {
    log, err := logger.NewZapLogger(&c.Config.Log)
    if err != nil {
        return err
    }
    c.Logger = log
    return nil
}

// initDatabase 初始化数据库
func (c *Container) initDatabase() error {
    db, err := config.NewDatabase(&c.Config.Database)
    if err != nil {
        return err
    }
    c.DB = db
    return nil
}

// initRedis 初始化Redis
func (c *Container) initRedis() error {
    rdb := redis.NewClient(&redis.Options{
        Addr:     fmt.Sprintf("%s:%d", c.Config.Redis.Host, c.Config.Redis.Port),
        Password: c.Config.Redis.Password,
        DB:       c.Config.Redis.DB,
    })
    c.Redis = rdb
    return nil
}

// initRepository 初始化仓储层
func (c *Container) initRepository() {
    c.Repository = repository.NewRepository(c.DB, c.Logger)
}

// initService 初始化服务层
func (c *Container) initService() {
    c.Service = service.NewServices(c.Repository, c.Redis, c.Logger, c.Config)
}

// initHandler 初始化处理器层
func (c *Container) initHandler() {
    c.Handler = handler.NewHandlers(c.Service, c.Logger)
}

错误处理 #

统一错误定义 #

// pkg/response/error.go
package response

import (
    "fmt"
    "net/http"
)

// Error 自定义错误
type Error struct {
    Code    int    `json:"code"`
    Message string `json:"message"`
    Details string `json:"details,omitempty"`
}

// Error 实现error接口
func (e *Error) Error() string {
    return fmt.Sprintf("code: %d, message: %s", e.Code, e.Message)
}

// NewError 创建错误
func NewError(code int, message, details string) *Error {
    return &Error{
        Code:    code,
        Message: message,
        Details: details,
    }
}

// 预定义错误
var (
    ErrBadRequest          = NewError(http.StatusBadRequest, "请求参数错误", "")
    ErrUnauthorized        = NewError(http.StatusUnauthorized, "未授权访问", "")
    ErrForbidden          = NewError(http.StatusForbidden, "禁止访问", "")
    ErrNotFound           = NewError(http.StatusNotFound, "资源不存在", "")
    ErrConflict           = NewError(http.StatusConflict, "资源冲突", "")
    ErrInternalServer     = NewError(http.StatusInternalServerError, "服务器内部错误", "")
    ErrServiceUnavailable = NewError(http.StatusServiceUnavailable, "服务不可用", "")
)

// 业务错误
var (
    ErrUserNotFound     = NewError(40001, "用户不存在", "")
    ErrUserExists       = NewError(40002, "用户已存在", "")
    ErrInvalidPassword  = NewError(40003, "密码错误", "")
    ErrArticleNotFound  = NewError(40004, "文章不存在", "")
    ErrCommentNotFound  = NewError(40005, "评论不存在", "")
    ErrInvalidToken     = NewError(40006, "无效的令牌", "")
    ErrTokenExpired     = NewError(40007, "令牌已过期", "")
)

统一响应格式 #

// pkg/response/response.go
package response

import (
    "net/http"
    "github.com/gin-gonic/gin"
)

// Response 统一响应格式
type Response struct {
    Code    int         `json:"code"`
    Message string      `json:"message"`
    Data    interface{} `json:"data,omitempty"`
}

// PageResponse 分页响应
type PageResponse struct {
    List       interface{} `json:"list"`
    Total      int64       `json:"total"`
    Page       int         `json:"page"`
    PageSize   int         `json:"page_size"`
    TotalPages int         `json:"total_pages"`
}

// Success 成功响应
func Success(c *gin.Context, data interface{}) {
    c.JSON(http.StatusOK, Response{
        Code:    0,
        Message: "success",
        Data:    data,
    })
}

// SuccessWithMessage 带消息的成功响应
func SuccessWithMessage(c *gin.Context, message string, data interface{}) {
    c.JSON(http.StatusOK, Response{
        Code:    0,
        Message: message,
        Data:    data,
    })
}

// Error 错误响应
func Error(c *gin.Context, err *Error) {
    httpStatus := http.StatusInternalServerError
    if err.Code < 50000 {
        httpStatus = err.Code
    }

    c.JSON(httpStatus, Response{
        Code:    err.Code,
        Message: err.Message,
    })
}

// Page 分页响应
func Page(c *gin.Context, list interface{}, total int64, page, pageSize int) {
    totalPages := int(total) / pageSize
    if int(total)%pageSize > 0 {
        totalPages++
    }

    Success(c, PageResponse{
        List:       list,
        Total:      total,
        Page:       page,
        PageSize:   pageSize,
        TotalPages: totalPages,
    })
}

应用程序入口 #

// cmd/server/main.go
package main

import (
    "context"
    "flag"
    "fmt"
    "log"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"

    "blog-system/internal/config"
    "blog-system/internal/container"
    "blog-system/internal/router"
)

var configPath = flag.String("config", "configs/config.yaml", "配置文件路径")

func main() {
    flag.Parse()

    // 加载配置
    cfg, err := config.Load(*configPath)
    if err != nil {
        log.Fatalf("加载配置失败: %v", err)
    }

    // 创建依赖注入容器
    c, err := container.NewContainer(cfg)
    if err != nil {
        log.Fatalf("初始化容器失败: %v", err)
    }

    // 创建路由
    r := router.NewRouter(c)

    // 创建HTTP服务器
    server := &http.Server{
        Addr:         fmt.Sprintf(":%d", cfg.Server.Port),
        Handler:      r,
        ReadTimeout:  cfg.Server.ReadTimeout,
        WriteTimeout: cfg.Server.WriteTimeout,
    }

    // 启动服务器
    go func() {
        c.Logger.Info("服务器启动", "port", cfg.Server.Port)
        if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            c.Logger.Fatal("服务器启动失败", "error", err)
        }
    }()

    // 优雅关闭
    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    <-quit

    c.Logger.Info("正在关闭服务器...")

    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()

    if err := server.Shutdown(ctx); err != nil {
        c.Logger.Fatal("服务器关闭失败", "error", err)
    }

    c.Logger.Info("服务器已关闭")
}

项目初始化脚本 #

#!/bin/bash
# scripts/init.sh

# 创建必要的目录
mkdir -p storage/logs
mkdir -p storage/uploads
mkdir -p docs/swagger

# 初始化Go模块
go mod init blog-system

# 下载依赖
go mod tidy

# 生成Swagger文档
swag init -g cmd/server/main.go -o docs/swagger

echo "项目初始化完成!"

总结 #

本节介绍了 Go Web 项目的架构设计,包括:

  1. 清晰的分层架构:表现层、业务层、数据层职责分明
  2. 合理的目录结构:便于维护和扩展
  3. 灵活的配置管理:支持多环境配置
  4. 完善的依赖注入:降低组件耦合度
  5. 统一的错误处理:提供一致的错误响应
  6. 优雅的启动关闭:确保服务稳定运行

这样的架构设计为后续功能开发奠定了坚实的基础,确保项目的可维护性和可扩展性。