项目架构:构建可维护的 Go 应用
当你的 Go 项目从几十行代码的小工具,成长为成千上万行代码的大型应用时,一个清晰的项目架构就变得至关重要了。
好的架构能让代码:
- 易于理解:新人能快速找到相关代码
- 易于测试:各层职责清晰,便于单元测试
- 易于维护:修改一处不会影响其他部分
- 易于扩展:新功能可以无缝集成
今天我们就来学习如何设计和组织一个可维护的 Go 项目。
标准项目布局
Go 社区推荐的项目布局(参考 golang-standards/project-layout):
myproject/
├── cmd/ # 应用程序入口
│ ├── server/
│ │ └── main.go # Web 服务器入口
│ ├── worker/
│ │ └── main.go # 后台任务入口
│ └── cli/
│ └── main.go # CLI 工具入口
├── internal/ # 私有代码(不会被外部项目导入)
│ ├── handler/ # HTTP 处理器
│ ├── service/ # 业务逻辑
│ ├── repository/ # 数据访问
│ ├── model/ # 数据模型
│ └── middleware/ # 中间件
├── pkg/ # 公共库(可以被外部项目导入)
│ ├── logger/
│ ├── config/
│ └── utils/
├── api/ # API 定义(OpenAPI/Swagger、gRPC proto)
├── configs/ # 配置文件
├── scripts/ # 脚本
├── deployments/ # 部署配置(Docker、K8s)
├── test/ # 测试数据和工具
├── docs/ # 文档
├── go.mod
├── go.sum
├── Dockerfile
├── Makefile
└── README.md
关键目录说明
cmd/:应用程序的入口点。每个子目录对应一个可执行文件。
// cmd/server/main.go
package main
import (
"log"
"myproject/internal/server"
"myproject/pkg/config"
)
func main() {
cfg, err := config.Load()
if err != nil {
log.Fatal(err)
}
server.Run(cfg)
}
internal/:Go 的特殊目录,其中的代码只能被同模块的代码导入。这强制了模块的边界。
pkg/:可以被外部项目导入的公共库。
分层架构
经典的分层架构将应用分为几层:
┌─────────────────────────────────────┐
│ Presentation Layer │ HTTP/gRPC handlers
├─────────────────────────────────────┤
│ Business Layer │ 业务逻辑(services)
├─────────────────────────────────────┤
│ Data Access Layer │ 数据访问(repositories)
├─────────────────────────────────────┤
│ Database │ MySQL/PostgreSQL/Redis
└─────────────────────────────────────┘
实战:用户管理服务
让我们实现一个完整的用户管理服务。
Model 层
// internal/model/user.go
package model
import "time"
type User struct {
ID int64 `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Password string `json:"-"` // 不序列化到 JSON
Age int `json:"age"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type CreateUserRequest struct {
Name string `json:"name" binding:"required"`
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required,min=6"`
Age int `json:"age" binding:"gte=0,lte=150"`
}
type UpdateUserRequest struct {
Name string `json:"name,omitempty"`
Email string `json:"email,omitempty"`
Age int `json:"age,omitempty"`
}
Repository 层
// internal/repository/user_repository.go
package repository
import (
"context"
"database/sql"
"myproject/internal/model"
)
// UserRepository 用户数据访问接口
type UserRepository interface {
Create(ctx context.Context, user *model.User) error
GetByID(ctx context.Context, id int64) (*model.User, error)
GetByEmail(ctx context.Context, email string) (*model.User, error)
Update(ctx context.Context, user *model.User) error
Delete(ctx context.Context, id int64) error
List(ctx context.Context, offset, limit int) ([]*model.User, error)
}
// mysqlUserRepository MySQL 实现
type mysqlUserRepository struct {
db *sql.DB
}
func NewMySQLUserRepository(db *sql.DB) UserRepository {
return &mysqlUserRepository{db: db}
}
func (r *mysqlUserRepository) Create(ctx context.Context, user *model.User) error {
query := `
INSERT INTO users (name, email, password, age, created_at, updated_at)
VALUES (?, ?, ?, ?, NOW(), NOW())
`
result, err := r.db.ExecContext(ctx, query,
user.Name, user.Email, user.Password, user.Age)
if err != nil {
return err
}
id, err := result.LastInsertId()
if err != nil {
return err
}
user.ID = id
return nil
}
func (r *mysqlUserRepository) GetByID(ctx context.Context, id int64) (*model.User, error) {
query := `
SELECT id, name, email, password, age, created_at, updated_at
FROM users
WHERE id = ?
`
user := &model.User{}
err := r.db.QueryRowContext(ctx, query, id).Scan(
&user.ID, &user.Name, &user.Email, &user.Password,
&user.Age, &user.CreatedAt, &user.UpdatedAt,
)
if err == sql.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, err
}
return user, nil
}
// ... 其他方法实现
Service 层
// internal/service/user_service.go
package service
import (
"context"
"errors"
"fmt"
"golang.org/x/crypto/bcrypt"
"myproject/internal/model"
"myproject/internal/repository"
)
// 错误定义
var (
ErrUserNotFound = errors.New("user not found")
ErrEmailExists = errors.New("email already exists")
ErrInvalidInput = errors.New("invalid input")
)
// UserService 用户业务逻辑接口
type UserService interface {
CreateUser(ctx context.Context, req *model.CreateUserRequest) (*model.User, error)
GetUser(ctx context.Context, id int64) (*model.User, error)
UpdateUser(ctx context.Context, id int64, req *model.UpdateUserRequest) (*model.User, error)
DeleteUser(ctx context.Context, id int64) error
ListUsers(ctx context.Context, page, pageSize int) ([]*model.User, error)
}
// userService 实现
type userService struct {
repo repository.UserRepository
}
func NewUserService(repo repository.UserRepository) UserService {
return &userService{repo: repo}
}
func (s *userService) CreateUser(ctx context.Context, req *model.CreateUserRequest) (*model.User, error) {
// 检查邮箱是否已存在
existing, err := s.repo.GetByEmail(ctx, req.Email)
if err != nil {
return nil, fmt.Errorf("查询邮箱失败: %w", err)
}
if existing != nil {
return nil, ErrEmailExists
}
// 哈希密码
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
if err != nil {
return nil, fmt.Errorf("密码哈希失败: %w", err)
}
user := &model.User{
Name: req.Name,
Email: req.Email,
Password: string(hashedPassword),
Age: req.Age,
}
if err := s.repo.Create(ctx, user); err != nil {
return nil, fmt.Errorf("创建用户失败: %w", err)
}
return user, nil
}
func (s *userService) GetUser(ctx context.Context, id int64) (*model.User, error) {
user, err := s.repo.GetByID(ctx, id)
if err != nil {
return nil, fmt.Errorf("查询用户失败: %w", err)
}
if user == nil {
return nil, ErrUserNotFound
}
return user, nil
}
func (s *userService) UpdateUser(ctx context.Context, id int64, req *model.UpdateUserRequest) (*model.User, error) {
user, err := s.GetUser(ctx, id)
if err != nil {
return nil, err
}
// 更新字段
if req.Name != "" {
user.Name = req.Name
}
if req.Email != "" {
// 检查新邮箱是否已存在
existing, err := s.repo.GetByEmail(ctx, req.Email)
if err != nil {
return nil, err
}
if existing != nil && existing.ID != id {
return nil, ErrEmailExists
}
user.Email = req.Email
}
if req.Age > 0 {
user.Age = req.Age
}
if err := s.repo.Update(ctx, user); err != nil {
return nil, fmt.Errorf("更新用户失败: %w", err)
}
return user, nil
}
func (s *userService) DeleteUser(ctx context.Context, id int64) error {
user, err := s.GetUser(ctx, id)
if err != nil {
return err
}
if user == nil {
return ErrUserNotFound
}
return s.repo.Delete(ctx, id)
}
func (s *userService) ListUsers(ctx context.Context, page, pageSize int) ([]*model.User, error) {
if page < 1 {
page = 1
}
if pageSize < 1 || pageSize > 100 {
pageSize = 20
}
offset := (page - 1) * pageSize
return s.repo.List(ctx, offset, pageSize)
}
Handler 层
// internal/handler/user_handler.go
package handler
import (
"encoding/json"
"net/http"
"strconv"
"github.com/gorilla/mux"
"myproject/internal/model"
"myproject/internal/service"
)
type UserHandler struct {
service service.UserService
}
func NewUserHandler(service service.UserService) *UserHandler {
return &UserHandler{service: service}
}
func (h *UserHandler) RegisterRoutes(r *mux.Router) {
r.HandleFunc("/users", h.Create).Methods("POST")
r.HandleFunc("/users/{id}", h.Get).Methods("GET")
r.HandleFunc("/users/{id}", h.Update).Methods("PUT")
r.HandleFunc("/users/{id}", h.Delete).Methods("DELETE")
r.HandleFunc("/users", h.List).Methods("GET")
}
func (h *UserHandler) Create(w http.ResponseWriter, r *http.Request) {
var req model.CreateUserRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondError(w, http.StatusBadRequest, "无效的请求体")
return
}
user, err := h.service.CreateUser(r.Context(), &req)
if err != nil {
switch err {
case service.ErrEmailExists:
respondError(w, http.StatusConflict, err.Error())
default:
respondError(w, http.StatusInternalServerError, err.Error())
}
return
}
respondJSON(w, http.StatusCreated, user)
}
func (h *UserHandler) Get(w http.ResponseWriter, r *http.Request) {
id, err := strconv.ParseInt(mux.Vars(r)["id"], 10, 64)
if err != nil {
respondError(w, http.StatusBadRequest, "无效的用户 ID")
return
}
user, err := h.service.GetUser(r.Context(), id)
if err != nil {
if err == service.ErrUserNotFound {
respondError(w, http.StatusNotFound, err.Error())
} else {
respondError(w, http.StatusInternalServerError, err.Error())
}
return
}
respondJSON(w, http.StatusOK, user)
}
// ... Update, Delete, List 方法
func respondJSON(w http.ResponseWriter, status int, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(data)
}
func respondError(w http.ResponseWriter, status int, message string) {
respondJSON(w, status, map[string]string{"error": message})
}
应用入口
// cmd/server/main.go
package main
import (
"database/sql"
"log"
"net/http"
"github.com/gorilla/mux"
_ "github.com/go-sql-driver/mysql"
"myproject/internal/handler"
"myproject/internal/repository"
"myproject/internal/service"
"myproject/pkg/config"
)
func main() {
// 加载配置
cfg, err := config.Load()
if err != nil {
log.Fatal(err)
}
// 连接数据库
db, err := sql.Open("mysql", cfg.Database.DSN())
if err != nil {
log.Fatal(err)
}
defer db.Close()
// 依赖注入
userRepo := repository.NewMySQLUserRepository(db)
userService := service.NewUserService(userRepo)
userHandler := handler.NewUserHandler(userService)
// 设置路由
r := mux.NewRouter()
userHandler.RegisterRoutes(r)
// 启动服务器
addr := ":" + cfg.Server.Port
log.Printf("服务器启动在 %s", addr)
log.Fatal(http.ListenAndServe(addr, r))
}
依赖注入
依赖注入(DI)是一种设计模式,它将依赖的创建和使用分离。这让代码更容易测试和维护。
手动依赖注入
上面的例子就是手动依赖注入。在 main.go 中,我们创建所有的依赖并组装它们。
使用 Wire
Google 的 Wire 是一个编译时依赖注入工具:
// cmd/server/wire.go
//go:build wireinject
// +build wireinject
package main
import (
"github.com/google/wire"
"myproject/internal/handler"
"myproject/internal/repository"
"myproject/internal/service"
)
func InitializeUserHandler(db *sql.DB) *handler.UserHandler {
wire.Build(
repository.NewMySQLUserRepository,
service.NewUserService,
handler.NewUserHandler,
)
return nil
}
生成代码:
wire
测试策略
Repository 层测试
使用内存数据库(SQLite)或 mock:
func TestMySQLUserRepository_Create(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
repo := repository.NewMySQLUserRepository(db)
user := &model.User{
Name: "张三",
Email: "zhangsan@example.com",
Age: 25,
}
err := repo.Create(context.Background(), user)
assert.NoError(t, err)
assert.NotZero(t, user.ID)
// 验证数据
retrieved, err := repo.GetByID(context.Background(), user.ID)
assert.NoError(t, err)
assert.Equal(t, user.Name, retrieved.Name)
}
Service 层测试
使用 mock repository:
type MockUserRepository struct {
users map[int64]*model.User
}
func (m *MockUserRepository) Create(ctx context.Context, user *model.User) error {
user.ID = int64(len(m.users) + 1)
m.users[user.ID] = user
return nil
}
func TestUserService_CreateUser(t *testing.T) {
mockRepo := &MockUserRepository{users: make(map[int64]*model.User)}
service := service.NewUserService(mockRepo)
req := &model.CreateUserRequest{
Name: "张三",
Email: "zhangsan@example.com",
Password: "password123",
Age: 25,
}
user, err := service.CreateUser(context.Background(), req)
assert.NoError(t, err)
assert.NotZero(t, user.ID)
assert.Equal(t, req.Name, user.Name)
}
配置管理
// pkg/config/config.go
package config
import (
"fmt"
"github.com/spf13/viper"
)
type Config struct {
Server ServerConfig
Database DatabaseConfig
Log LogConfig
}
type ServerConfig struct {
Host string
Port string
}
type DatabaseConfig struct {
Host string
Port int
User string
Password string
Name string
}
func (c *DatabaseConfig) DSN() string {
return fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?parseTime=true",
c.User, c.Password, c.Host, c.Port, c.Name)
}
type LogConfig struct {
Level string
}
func Load() (*Config, error) {
viper.SetConfigName("config")
viper.SetConfigType("yaml")
viper.AddConfigPath(".")
viper.AddConfigPath("./configs")
viper.AutomaticEnv()
if err := viper.ReadInConfig(); err != nil {
return nil, err
}
var cfg Config
if err := viper.Unmarshal(&cfg); err != nil {
return nil, err
}
return &cfg, nil
}
小结
今天我们学习了 Go 项目的架构设计:
- 项目布局:标准的目录结构
- 分层架构:Handler → Service → Repository
- 依赖注入:手动注入和 Wire 工具
- 测试策略:各层的测试方法
- 配置管理:使用 Viper
好的架构不是一蹴而就的,而是随着项目的发展逐步演进的。从小项目开始,保持代码的清晰和可测试性,当项目变大时,架构自然会变得更好。
系列总结
恭喜你完成了 Go 语言入门系列的全部 40 篇文章!🎉
从基础语法到高级特性,从并发编程到性能优化,从数据库操作到项目架构,你已经掌握了构建真实 Go 应用所需的核心知识。
学习路径回顾:
- 第 1-10 篇:Go 基础(变量、控制流、函数、数据结构)
- 第 11-20 篇:进阶特性(并发、I/O、网络、测试)
- 第 21-30 篇:实战应用(数据库、Web、工具)
- 第 31-40 篇:工程实践(安全、缓存、部署、架构)
下一步建议:
- 实践项目:选择一个真实的项目,从头到尾实现它
- 阅读源码:阅读优秀的 Go 开源项目(如 Docker、Kubernetes)
- 持续学习:关注 Go 官方博客、参加 Go 社区活动
- 分享知识:写博客、做分享,教是最好的学
Go 语言简洁而强大,它的设计哲学是"少即是多"。希望这个系列能帮助你掌握 Go,用它构建出优秀的软件!
继续前进,Go 的未来属于你!🚀
参考资料:
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。