Go 项目结构:构建可维护的大型项目

深入探讨 Go 项目的标准布局,包括 cmd、internal、pkg 目录设计,依赖管理,构建自动化和 CI/CD 配置

Go 项目结构:构建可维护的大型项目

你是否遇到过这样的场景:打开一个 Go 项目,看到所有代码都堆在 main.go 里,或者目录结构混乱得像一盘意大利面?随着项目规模增长,这种混乱的结构会让你每次修改代码都像是在拆炸弹——稍有不慎就会引发连锁反应。

一个好的项目结构就像一张清晰的地图,让新同事第一天入职就能快速找到方向,让资深开发者能够从容地进行重构。今天,我们就来聊聊如何设计一个可维护、可扩展的 Go 项目结构。

标准 Go 项目布局

首先,让我们看一个经过社区验证的标准项目布局:

myproject/
├── cmd/                    # 应用程序入口
│   ├── server/
│   │   └── main.go
│   ├── cli/
│   │   └── main.go
│   └── worker/
│       └── main.go
├── internal/               # 私有代码(不对外暴露)
│   ├── app/
│   │   ├── server.go
│   │   └── config.go
│   ├── domain/
│   │   ├── user.go
│   │   └── order.go
│   ├── handler/
│   │   ├── user_handler.go
│   │   └── order_handler.go
│   ├── repository/
│   │   ├── user_repo.go
│   │   └── order_repo.go
│   └── service/
│       ├── user_service.go
│       └── order_service.go
├── pkg/                    # 可被外部项目使用的公共库
│   ├── httpclient/
│   │   └── client.go
│   ├── logger/
│   │   └── logger.go
│   └── validator/
│       └── validator.go
├── api/                    # API 定义文件
│   ├── openapi.yaml
│   └── protobuf/
│       └── user.proto
├── configs/                # 配置文件模板
│   ├── config.yaml.example
│   └── config.dev.yaml
├── scripts/                # 构建、测试、部署脚本
│   ├── build.sh
│   └── deploy.sh
├── deployments/            # 部署配置
│   ├── docker/
│   │   └── Dockerfile
│   └── k8s/
│       └── deployment.yaml
├── docs/                   # 项目文档
│   ├── architecture.md
│   └── api.md
├── test/                   # 集成测试和端到端测试
│   ├── integration/
│   └── e2e/
├── go.mod
├── go.sum
├── Makefile
├── README.md
└── .gitignore

这个布局看起来有点复杂?别担心,我们来逐一拆解每个部分。

cmd 目录:应用程序入口

cmd 目录存放应用程序的入口点。每个子目录代表一个可执行文件,里面只有一个精简的 main.go

为什么要把入口点放在 cmd 里?

想象一下,你的项目需要同时提供 HTTP 服务、命令行工具和后台 Worker。如果所有代码都塞在一个 main.go 里,会变成什么样?

// ❌ 反面教材:一个巨大的 main.go
package main

import (
    "fmt"
    "net/http"
    "os"
)

func main() {
    if len(os.Args) < 2 {
        fmt.Println("Usage: myapp [server|cli|worker]")
        return
    }

    switch os.Args[1] {
    case "server":
        // 100 行启动服务器的代码...
    case "cli":
        // 50 行命令行处理的代码...
    case "worker":
        // 80 行 Worker 启动的代码...
    }
}

这种写法的问题显而易见:职责混乱、难以测试、编译时间长。

正确的做法

将每个入口点拆分为独立的可执行文件:

// cmd/server/main.go
package main

import (
    "log"
    "myproject/internal/app"
    "myproject/internal/config"
)

func main() {
    // 1. 加载配置
    cfg, err := config.Load()
    if err != nil {
        log.Fatalf("Failed to load config: %v", err)
    }

    // 2. 创建应用
    application, err := app.NewServer(cfg)
    if err != nil {
        log.Fatalf("Failed to create server: %v", err)
    }

    // 3. 启动服务
    if err := application.Run(); err != nil {
        log.Fatalf("Server failed: %v", err)
    }
}
// cmd/cli/main.go
package main

import (
    "log"
    "myproject/internal/app"
)

func main() {
    cli := app.NewCLI()
    if err := cli.Execute(); err != nil {
        log.Fatalf("CLI failed: %v", err)
    }
}
// cmd/worker/main.go
package main

import (
    "log"
    "myproject/internal/app"
    "myproject/internal/config"
)

func main() {
    cfg, err := config.Load()
    if err != nil {
        log.Fatalf("Failed to load config: %v", err)
    }

    worker, err := app.NewWorker(cfg)
    if err != nil {
        log.Fatalf("Failed to create worker: %v", err)
    }

    if err := worker.Run(); err != nil {
        log.Fatalf("Worker failed: %v", err)
    }
}

这样做的好处:

  • 职责清晰:每个入口点只负责启动逻辑
  • 独立编译go build ./cmd/server 只编译服务器
  • 易于部署:不同的可执行文件可以部署到不同的环境

internal vs pkg:可见性的艺术

Go 语言有一个独特的特性:internal 目录下的代码不能被外部项目导入。这是一个编译器强制的可见性约束。

internal 目录:你的私有领地

internal 目录存放项目的核心业务逻辑,这些代码不应该被其他项目依赖:

// internal/domain/user.go
package domain

import (
    "errors"
    "regexp"
    "time"
)

// User 代表用户实体
type User struct {
    ID        int64
    Email     string
    Password  string
    CreatedAt time.Time
    UpdatedAt time.Time
}

// Validate 验证用户数据
func (u *User) Validate() error {
    if u.Email == "" {
        return errors.New("email is required")
    }

    emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
    if !emailRegex.MatchString(u.Email) {
        return errors.New("invalid email format")
    }

    if len(u.Password) < 8 {
        return errors.New("password must be at least 8 characters")
    }

    return nil
}
// internal/service/user_service.go
package service

import (
    "context"
    "errors"
    "myproject/internal/domain"
    "myproject/internal/repository"
)

// UserService 处理用户业务逻辑
type UserService struct {
    repo repository.UserRepository
}

// NewUserService 创建用户服务实例
func NewUserService(repo repository.UserRepository) *UserService {
    return &UserService{repo: repo}
}

// CreateUser 创建新用户
func (s *UserService) CreateUser(ctx context.Context, email, password string) (*domain.User, error) {
    // 检查用户是否已存在
    existing, _ := s.repo.FindByEmail(ctx, email)
    if existing != nil {
        return nil, errors.New("user already exists")
    }

    // 创建用户
    user := &domain.User{
        Email:    email,
        Password: password, // 实际应用中需要先加密
    }

    if err := user.Validate(); err != nil {
        return nil, err
    }

    if err := s.repo.Create(ctx, user); err != nil {
        return nil, err
    }

    return user, nil
}

pkg 目录:可复用的公共库

pkg 目录存放可以被其他项目导入的代码。比如你封装了一个通用的 HTTP 客户端:

// pkg/httpclient/client.go
package httpclient

import (
    "context"
    "net/http"
    "time"
)

// Client 是一个增强的 HTTP 客户端
type Client struct {
    httpClient *http.Client
    baseURL    string
    headers    map[string]string
}

// Option 是配置选项函数
type Option func(*Client)

// WithTimeout 设置超时时间
func WithTimeout(timeout time.Duration) Option {
    return func(c *Client) {
        c.httpClient.Timeout = timeout
    }
}

// WithHeader 添加默认请求头
func WithHeader(key, value string) Option {
    return func(c *Client) {
        c.headers[key] = value
    }
}

// NewClient 创建新的 HTTP 客户端
func NewClient(baseURL string, opts ...Option) *Client {
    c := &Client{
        httpClient: &http.Client{Timeout: 30 * time.Second},
        baseURL:    baseURL,
        headers:    make(map[string]string),
    }

    for _, opt := range opts {
        opt(c)
    }

    return c
}

// Get 发送 GET 请求
func (c *Client) Get(ctx context.Context, path string) (*http.Response, error) {
    req, err := http.NewRequestWithContext(ctx, "GET", c.baseURL+path, nil)
    if err != nil {
        return nil, err
    }

    for k, v := range c.headers {
        req.Header.Set(k, v)
    }

    return c.httpClient.Do(req)
}

何时使用 internal vs pkg?

使用 internal:

  • 业务逻辑(用户服务、订单服务)
  • 数据访问层(数据库操作)
  • 应用特定的配置
  • HTTP Handler

使用 pkg:

  • 通用的工具库(日志、验证器)
  • 可复用的中间件
  • 第三方服务的客户端封装

依赖管理:go.mod 的最佳实践

Go Modules 是 Go 的官方依赖管理系统。让我们看看如何正确使用它。

初始化模块

# 初始化新模块
go mod init github.com/yourname/myproject

# 添加依赖
go get github.com/gin-gonic/gin@v1.9.1
go get github.com/lib/pq@v1.10.9

# 整理依赖
go mod tidy

# 查看依赖树
go mod graph

版本管理策略

// go.mod
module github.com/yourname/myproject

go 1.21

require (
    github.com/gin-gonic/gin v1.9.1
    github.com/lib/pq v1.10.9
    github.com/redis/go-redis/v9 v9.3.0
)

// 使用 replace 指令处理特殊情况
replace github.com/problematic/package => github.com/forked/package v1.0.0

依赖版本锁定

# 查看当前依赖版本
go list -m all

# 升级到最新补丁版本
go get -u=patch github.com/gin-gonic/gin

# 锁定到特定版本
go get github.com/gin-gonic/gin@v1.9.1

# 生成 vendor 目录(可选)
go mod vendor

Makefile:自动化构建任务

一个好的 Makefile 可以让开发流程标准化。让我们创建一个实用的 Makefile:

# Makefile

# 变量定义
APP_NAME := myproject
VERSION := $(shell git describe --tags --always --dirty)
BUILD_TIME := $(shell date -u +"%Y-%m-%d_%H:%M:%S")
LDFLAGS := -ldflags "-X main.Version=$(VERSION) -X main.BuildTime=$(BUILD_TIME)"

# 默认目标
.DEFAULT_GOAL := help

.PHONY: help
help: ## 显示帮助信息
	@echo "可用命令:"
	@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'

.PHONY: build
build: ## 构建所有可执行文件
	@echo "🔨 构建服务器..."
	go build $(LDFLAGS) -o bin/server ./cmd/server
	@echo "🔨 构建 CLI..."
	go build $(LDFLAGS) -o bin/cli ./cmd/cli
	@echo "🔨 构建 Worker..."
	go build $(LDFLAGS) -o bin/worker ./cmd/worker
	@echo "✅ 构建完成"

.PHONY: test
test: ## 运行单元测试
	@echo "🧪 运行测试..."
	go test -v -race -coverprofile=coverage.out ./...
	go tool cover -html=coverage.out -o coverage.html
	@echo "✅ 测试完成,覆盖率报告:coverage.html"

.PHONY: lint
lint: ## 运行代码检查
	@echo "🔍 运行代码检查..."
	golangci-lint run
	@echo "✅ 代码检查完成"

.PHONY: fmt
fmt: ## 格式化代码
	@echo "✨ 格式化代码..."
	gofmt -s -w .
	goimports -w .
	@echo "✅ 格式化完成"

.PHONY: clean
clean: ## 清理构建产物
	@echo "🧹 清理..."
	rm -rf bin/
	rm -f coverage.out coverage.html
	@echo "✅ 清理完成"

.PHONY: docker-build
docker-build: ## 构建 Docker 镜像
	@echo "🐳 构建 Docker 镜像..."
	docker build -t $(APP_NAME):$(VERSION) -f deployments/docker/Dockerfile .
	@echo "✅ Docker 镜像构建完成"

.PHONY: docker-run
docker-run: ## 运行 Docker 容器
	docker run -p 8080:8080 --env-file .env $(APP_NAME):$(VERSION)

.PHONY: migrate-up
migrate-up: ## 运行数据库迁移
	@echo "📦 运行数据库迁移..."
	migrate -path migrations -database "postgres://user:pass@localhost:5432/mydb?sslmode=disable" up
	@echo "✅ 迁移完成"

.PHONY: migrate-down
migrate-down: ## 回滚数据库迁移
	@echo "⏪ 回滚数据库迁移..."
	migrate -path migrations -database "postgres://user:pass@localhost:5432/mydb?sslmode=disable" down 1
	@echo "✅ 回滚完成"

.PHONY: generate
generate: ## 运行代码生成
	@echo "⚙️  生成代码..."
	go generate ./...
	@echo "✅ 代码生成完成"

使用示例:

# 查看所有可用命令
make help

# 构建项目
make build

# 运行测试
make test

# 构建 Docker 镜像
make docker-build

Dockerfile:多阶段构建

一个优化的 Dockerfile 可以显著减小镜像体积:

# deployments/docker/Dockerfile

# ===== 构建阶段 =====
FROM golang:1.21-alpine AS builder

# 安装必要的工具
RUN apk add --no-cache git make

WORKDIR /build

# 复制依赖文件
COPY go.mod go.sum ./

# 下载依赖
RUN go mod download

# 复制源代码
COPY . .

# 构建应用
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o server ./cmd/server

# ===== 运行阶段 =====
FROM alpine:latest

# 安装必要的运行时依赖
RUN apk add --no-cache ca-certificates tzdata

WORKDIR /app

# 从构建阶段复制二进制文件
COPY --from=builder /build/server .

# 复制配置文件
COPY configs/config.yaml.example ./config.yaml

# 创建非 root 用户
RUN addgroup -g 1000 appgroup && \
    adduser -u 1000 -G appgroup -s /bin/sh -D appuser

# 切换到非 root 用户
USER appuser

# 暴露端口
EXPOSE 8080

# 健康检查
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1

# 启动应用
CMD ["./server"]

构建和运行:

# 构建镜像
docker build -t myproject:latest -f deployments/docker/Dockerfile .

# 运行容器
docker run -p 8080:8080 myproject:latest

CI/CD 配置:GitHub Actions

自动化测试和部署是现代开发流程的核心。这里是一个完整的 GitHub Actions 配置:

# .github/workflows/ci.yml
name: CI/CD Pipeline

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  test:
    name: Test
    runs-on: ubuntu-latest
    
    services:
      postgres:
        image: postgres:15
        env:
          POSTGRES_USER: test
          POSTGRES_PASSWORD: test
          POSTGRES_DB: testdb
        ports:
          - 5432:5432
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
    
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
      
      - name: Set up Go
        uses: actions/setup-go@v4
        with:
          go-version: '1.21'
      
      - name: Cache dependencies
        uses: actions/cache@v3
        with:
          path: |
            ~/.cache/go-build
            ~/go/pkg/mod
          key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
          restore-keys: |
            ${{ runner.os }}-go-
      
      - name: Install dependencies
        run: go mod download
      
      - name: Run linter
        uses: golangci/golangci-lint-action@v3
        with:
          version: latest
      
      - name: Run tests
        run: make test
        env:
          DATABASE_URL: postgres://test:test@localhost:5432/testdb?sslmode=disable
      
      - name: Upload coverage
        uses: codecov/codecov-action@v3
        with:
          file: ./coverage.out

  build:
    name: Build
    runs-on: ubuntu-latest
    needs: test
    
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
      
      - name: Set up Go
        uses: actions/setup-go@v4
        with:
          go-version: '1.21'
      
      - name: Build binaries
        run: make build
      
      - name: Upload artifacts
        uses: actions/upload-artifact@v3
        with:
          name: binaries
          path: bin/

  docker:
    name: Build and Push Docker Image
    runs-on: ubuntu-latest
    needs: test
    if: github.ref == 'refs/heads/main'
    
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
      
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3
      
      - name: Login to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}
      
      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: yourname/myproject
          tags: |
            type=ref,event=branch
            type=sha,prefix={{branch}}-
            type=raw,value=latest,enable={{is_default_branch}}
      
      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          file: deployments/docker/Dockerfile
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

Monorepo vs Multi-Repo:如何选择?

这是一个经典的架构决策问题。让我们对比两种方案:

Monorepo(单一仓库)

适用场景:

  • 多个服务共享大量代码
  • 团队规模较小(< 50 人)
  • 需要频繁跨项目修改

目录结构示例:

monorepo/
├── services/
│   ├── user-service/
│   │   ├── cmd/
│   │   ├── internal/
│   │   └── go.mod
│   ├── order-service/
│   │   ├── cmd/
│   │   ├── internal/
│   │   └── go.mod
│   └── payment-service/
│       ├── cmd/
│       ├── internal/
│       └── go.mod
├── shared/
│   ├── pkg/
│   │   ├── logger/
│   │   └── database/
│   └── proto/
├── tools/
└── go.work

使用 Go Workspaces:

# 初始化 workspace
go work init

# 添加模块
go work use ./services/user-service
go work use ./services/order-service
go work use ./shared/pkg
// go.work
go 1.21

use (
    ./services/user-service
    ./services/order-service
    ./shared/pkg
)

Multi-Repo(多仓库)

适用场景:

  • 大型团队(> 50 人)
  • 服务之间耦合度低
  • 需要独立的发布周期

优势:

  • 权限控制更精细
  • CI/CD 更快(只构建变更的服务)
  • 依赖管理更简单

劣势:

  • 跨项目修改更复杂
  • 共享代码需要版本管理

Clean Architecture 在 Go 中的实践

Clean Architecture(整洁架构)是一种分层设计模式,让我们看看如何在 Go 中实现:

internal/
├── domain/          # 核心业务逻辑
│   ├── user.go
│   └── order.go
├── usecase/         # 业务用例
│   ├── create_user.go
│   └── place_order.go
├── handler/         # 外部接口(HTTP、gRPC)
│   ├── http/
│   │   └── user_handler.go
│   └── grpc/
│       └── user_service.go
├── repository/      # 数据访问
│   ├── postgres/
│   │   └── user_repo.go
│   └── redis/
│       └── cache_repo.go
└── dependency/      # 依赖注入
    └── container.go

领域层(Domain)

// internal/domain/user.go
package domain

import (
    "context"
    "time"
)

// User 代表用户实体
type User struct {
    ID        int64
    Email     string
    Name      string
    CreatedAt time.Time
}

// UserRepository 定义数据访问接口
type UserRepository interface {
    FindByID(ctx context.Context, id int64) (*User, error)
    FindByEmail(ctx context.Context, email string) (*User, error)
    Create(ctx context.Context, user *User) error
    Update(ctx context.Context, user *User) error
    Delete(ctx context.Context, id int64) error
}

用例层(Use Case)

// internal/usecase/create_user.go
package usecase

import (
    "context"
    "errors"
    "myproject/internal/domain"
)

// CreateUserInput 创建用户的输入参数
type CreateUserInput struct {
    Email string
    Name  string
}

// CreateUserOutput 创建用户的输出结果
type CreateUserOutput struct {
    User *domain.User
}

// CreateUser 创建用户的用例
type CreateUser struct {
    userRepo domain.UserRepository
}

// NewCreateUser 创建 CreateUser 实例
func NewCreateUser(userRepo domain.UserRepository) *CreateUser {
    return &CreateUser{userRepo: userRepo}
}

// Execute 执行创建用户操作
func (uc *CreateUser) Execute(ctx context.Context, input CreateUserInput) (*CreateUserOutput, error) {
    // 验证输入
    if input.Email == "" {
        return nil, errors.New("email is required")
    }

    // 检查用户是否已存在
    existing, err := uc.userRepo.FindByEmail(ctx, input.Email)
    if err != nil {
        return nil, err
    }
    if existing != nil {
        return nil, errors.New("user already exists")
    }

    // 创建用户
    user := &domain.User{
        Email: input.Email,
        Name:  input.Name,
    }

    if err := uc.userRepo.Create(ctx, user); err != nil {
        return nil, err
    }

    return &CreateUserOutput{User: user}, nil
}

Handler 层

// internal/handler/http/user_handler.go
package http

import (
    "encoding/json"
    "net/http"
    "myproject/internal/usecase"
)

// UserHandler 处理用户相关的 HTTP 请求
type UserHandler struct {
    createUser *usecase.CreateUser
}

// NewUserHandler 创建 UserHandler 实例
func NewUserHandler(createUser *usecase.CreateUser) *UserHandler {
    return &UserHandler{createUser: createUser}
}

// CreateUserRequest 创建用户请求
type CreateUserRequest struct {
    Email string `json:"email"`
    Name  string `json:"name"`
}

// CreateUser 处理创建用户请求
func (h *UserHandler) CreateUser(w http.ResponseWriter, r *http.Request) {
    var req CreateUserRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        http.Error(w, "Invalid request", http.StatusBadRequest)
        return
    }

    input := usecase.CreateUserInput{
        Email: req.Email,
        Name:  req.Name,
    }

    output, err := h.createUser.Execute(r.Context(), input)
    if err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }

    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(output.User)
}

依赖注入

// internal/dependency/container.go
package dependency

import (
    "database/sql"
    "myproject/internal/handler/http"
    "myproject/internal/repository/postgres"
    "myproject/internal/usecase"
)

// Container 管理所有依赖
type Container struct {
    db *sql.DB
    
    // Repositories
    userRepo *postgres.UserRepository
    
    // Use Cases
    createUser *usecase.CreateUser
    
    // Handlers
    userHandler *http.UserHandler
}

// NewContainer 创建依赖容器
func NewContainer(db *sql.DB) *Container {
    c := &Container{db: db}
    c.initRepositories()
    c.initUseCases()
    c.initHandlers()
    return c
}

func (c *Container) initRepositories() {
    c.userRepo = postgres.NewUserRepository(c.db)
}

func (c *Container) initUseCases() {
    c.createUser = usecase.NewCreateUser(c.userRepo)
}

func (c *Container) initHandlers() {
    c.userHandler = http.NewUserHandler(c.createUser)
}

// UserHandler 返回用户处理器
func (c *Container) UserHandler() *http.UserHandler {
    return c.userHandler
}

实战:从零搭建一个项目

让我们用一个完整的例子来实践今天学到的知识:

# 创建项目目录
mkdir myshop && cd myshop

# 初始化 Go 模块
go mod init github.com/yourname/myshop

# 创建目录结构
mkdir -p cmd/{server,cli}
mkdir -p internal/{app,domain,handler,repository,service,config}
mkdir -p pkg/{logger,validator}
mkdir -p configs
mkdir -p deployments/docker
mkdir -p scripts

# 创建文件
touch cmd/server/main.go
touch cmd/cli/main.go
touch internal/domain/user.go
touch internal/service/user_service.go
touch internal/repository/user_repo.go
touch internal/handler/user_handler.go
touch internal/config/config.go
touch internal/app/server.go
touch pkg/logger/logger.go
touch Makefile
touch deployments/docker/Dockerfile

现在,你已经有了一个结构清晰、易于维护的 Go 项目!

总结

一个好的项目结构是项目成功的基石。今天我们学习了:

核心概念:

  • cmd 目录:存放应用入口点,每个子目录一个可执行文件
  • internal vs pkg:internal 是私有代码,pkg 是公共库
  • Clean Architecture:分层设计,依赖倒置

工具链:

  • go.mod:依赖管理和版本控制
  • Makefile:自动化构建任务
  • Dockerfile:多阶段构建优化镜像
  • GitHub Actions:CI/CD 自动化

架构决策:

  • Monorepo vs Multi-Repo:根据团队规模和服务耦合度选择
  • 依赖注入:使用容器管理依赖

记住,没有完美的项目结构,只有适合你团队和项目阶段的结构。关键是保持一致性,让每个开发者都能快速理解和贡献代码。

下一篇,我们将探讨 Go 的安全编程实践,学习如何防范常见的安全漏洞。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页