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 项目的架构设计,包括:
- 清晰的分层架构:表现层、业务层、数据层职责分明
- 合理的目录结构:便于维护和扩展
- 灵活的配置管理:支持多环境配置
- 完善的依赖注入:降低组件耦合度
- 统一的错误处理:提供一致的错误响应
- 优雅的启动关闭:确保服务稳定运行
这样的架构设计为后续功能开发奠定了坚实的基础,确保项目的可维护性和可扩展性。