tianyuan-api-server/cmd目录详解.md
2025-07-13 20:37:12 +08:00

9.2 KiB
Raw Blame History

Go 项目中的 cmd 目录详解

🎯 cmd 目录的作用

cmd 目录是 Go 项目的标准目录布局,专门用来存放可执行程序的入口点。每个子目录代表一个不同的应用程序。

📁 目录结构示例

user-service/
├── cmd/                          # 应用程序入口目录
│   ├── server/                   # HTTP/gRPC 服务器
│   │   └── main.go              # 服务器启动入口
│   ├── cli/                     # 命令行工具
│   │   └── main.go              # CLI 工具入口
│   ├── worker/                  # 后台任务处理器
│   │   └── main.go              # Worker 入口
│   └── migrator/                # 数据库迁移工具
│       └── main.go              # 迁移工具入口
├── internal/                    # 内部业务逻辑
├── api/                         # API 定义
└── pkg/                         # 可复用的包

🔧 具体示例

1. 服务器入口 (cmd/server/main.go)

package main

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

    "google.golang.org/grpc"
    "user-service/internal/config"
    "user-service/internal/server"
    "user-service/internal/service"
)

func main() {
    // 1. 解析命令行参数
    var (
        configFile = flag.String("config", "configs/config.yaml", "配置文件路径")
        port       = flag.Int("port", 8080, "服务端口")
    )
    flag.Parse()

    // 2. 加载配置
    cfg, err := config.Load(*configFile)
    if err != nil {
        log.Fatal("加载配置失败:", err)
    }

    // 3. 初始化服务
    userService := service.NewUserService(cfg)

    // 4. 创建 gRPC 服务器
    grpcServer := grpc.NewServer()
    server.RegisterUserServer(grpcServer, userService)

    // 5. 启动服务器
    lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))
    if err != nil {
        log.Fatal("监听端口失败:", err)
    }

    // 6. 优雅关闭
    go func() {
        log.Printf("用户服务启动在端口 %d", *port)
        if err := grpcServer.Serve(lis); err != nil {
            log.Fatal("服务启动失败:", err)
        }
    }()

    // 7. 等待中断信号
    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    <-quit

    log.Println("正在关闭服务...")
    grpcServer.GracefulStop()
    log.Println("服务已关闭")
}

2. CLI 工具入口 (cmd/cli/main.go)

package main

import (
    "flag"
    "fmt"
    "os"

    "user-service/internal/config"
    "user-service/internal/service"
)

func main() {
    var (
        action = flag.String("action", "", "执行的动作: create-user, list-users, reset-password")
        userID = flag.Int64("user-id", 0, "用户ID")
        username = flag.String("username", "", "用户名")
    )
    flag.Parse()

    cfg, err := config.Load("configs/config.yaml")
    if err != nil {
        fmt.Printf("加载配置失败: %v\n", err)
        os.Exit(1)
    }

    userService := service.NewUserService(cfg)

    switch *action {
    case "create-user":
        if *username == "" {
            fmt.Println("用户名不能为空")
            os.Exit(1)
        }
        user, err := userService.CreateUser(*username)
        if err != nil {
            fmt.Printf("创建用户失败: %v\n", err)
            os.Exit(1)
        }
        fmt.Printf("用户创建成功: ID=%d, Username=%s\n", user.ID, user.Username)

    case "list-users":
        users, err := userService.ListUsers()
        if err != nil {
            fmt.Printf("获取用户列表失败: %v\n", err)
            os.Exit(1)
        }
        for _, user := range users {
            fmt.Printf("ID: %d, Username: %s, Email: %s\n", user.ID, user.Username, user.Email)
        }

    case "reset-password":
        if *userID == 0 {
            fmt.Println("用户ID不能为空")
            os.Exit(1)
        }
        newPassword, err := userService.ResetPassword(*userID)
        if err != nil {
            fmt.Printf("重置密码失败: %v\n", err)
            os.Exit(1)
        }
        fmt.Printf("密码重置成功,新密码: %s\n", newPassword)

    default:
        fmt.Println("不支持的操作,支持的操作: create-user, list-users, reset-password")
        os.Exit(1)
    }
}

3. Worker 入口 (cmd/worker/main.go)

package main

import (
    "context"
    "log"
    "os"
    "os/signal"
    "syscall"
    "time"

    "user-service/internal/config"
    "user-service/internal/worker"
)

func main() {
    cfg, err := config.Load("configs/config.yaml")
    if err != nil {
        log.Fatal("加载配置失败:", err)
    }

    // 创建上下文
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()

    // 初始化 Worker
    emailWorker := worker.NewEmailWorker(cfg)
    notificationWorker := worker.NewNotificationWorker(cfg)

    // 启动 Workers
    go func() {
        log.Println("启动邮件处理 Worker...")
        if err := emailWorker.Start(ctx); err != nil {
            log.Printf("邮件 Worker 错误: %v", err)
        }
    }()

    go func() {
        log.Println("启动通知处理 Worker...")
        if err := notificationWorker.Start(ctx); err != nil {
            log.Printf("通知 Worker 错误: %v", err)
        }
    }()

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

    log.Println("正在关闭 Workers...")
    cancel()

    // 等待 Workers 完成
    time.Sleep(5 * time.Second)
    log.Println("Workers 已关闭")
}

4. 数据库迁移工具 (cmd/migrator/main.go)

package main

import (
    "flag"
    "log"

    "user-service/internal/config"
    "user-service/internal/migration"
)

func main() {
    var (
        direction = flag.String("direction", "up", "迁移方向: up 或 down")
        steps     = flag.Int("steps", 0, "迁移步数0表示全部")
    )
    flag.Parse()

    cfg, err := config.Load("configs/config.yaml")
    if err != nil {
        log.Fatal("加载配置失败:", err)
    }

    migrator := migration.NewMigrator(cfg.Database.URL)

    switch *direction {
    case "up":
        log.Println("执行数据库迁移...")
        if err := migrator.Up(*steps); err != nil {
            log.Fatal("迁移失败:", err)
        }
        log.Println("迁移成功")

    case "down":
        log.Println("回滚数据库迁移...")
        if err := migrator.Down(*steps); err != nil {
            log.Fatal("回滚失败:", err)
        }
        log.Println("回滚成功")

    default:
        log.Fatal("不支持的迁移方向:", *direction)
    }
}

🎯 为什么这样设计?

1. 关注点分离

  • cmd/ 只负责程序启动和配置解析
  • internal/ 负责具体的业务逻辑
  • 每个应用程序有独立的入口点

2. 多种运行模式

# 启动 HTTP/gRPC 服务器
./user-service-server -port=8080 -config=prod.yaml

# 使用 CLI 工具
./user-service-cli -action=create-user -username=john

# 启动后台 Worker
./user-service-worker

# 执行数据库迁移
./user-service-migrator -direction=up

3. 构建和部署灵活性

# Dockerfile 示例
FROM golang:1.20-alpine AS builder

WORKDIR /app
COPY . .

# 分别构建不同的应用程序
RUN go build -o server ./cmd/server
RUN go build -o cli ./cmd/cli
RUN go build -o worker ./cmd/worker
RUN go build -o migrator ./cmd/migrator

FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/

# 根据需要复制不同的可执行文件
COPY --from=builder /app/server .
COPY --from=builder /app/migrator .

# 可以选择启动不同的程序
CMD ["./server"]

4. Makefile 示例

# Makefile
.PHONY: build-server build-cli build-worker build-migrator

build-server:
	go build -o bin/server ./cmd/server

build-cli:
	go build -o bin/cli ./cmd/cli

build-worker:
	go build -o bin/worker ./cmd/worker

build-migrator:
	go build -o bin/migrator ./cmd/migrator

build-all: build-server build-cli build-worker build-migrator

run-server:
	./bin/server -port=8080

run-worker:
	./bin/worker

migrate-up:
	./bin/migrator -direction=up

migrate-down:
	./bin/migrator -direction=down -steps=1

🚀 在你的项目中的应用

在你当前的项目中,每个服务都应该有这样的结构:

apps/user/
├── cmd/
│   ├── server/main.go        # gRPC 服务器
│   ├── cli/main.go          # 用户管理 CLI
│   └── migrator/main.go     # 数据库迁移
├── internal/                # 业务逻辑
├── user.proto              # API 定义
└── Dockerfile

apps/gateway/
├── cmd/
│   ├── server/main.go        # HTTP 网关服务器
│   └── cli/main.go          # 网关管理 CLI
├── internal/
├── gateway.api
└── Dockerfile

📋 总结

cmd 目录的核心作用:

  1. 程序入口点 - 每个 main.go 是一个独立的应用程序
  2. 配置解析 - 处理命令行参数和配置文件
  3. 依赖注入 - 初始化和连接各个组件
  4. 生命周期管理 - 启动、运行、优雅关闭
  5. 多种运行模式 - 服务器、CLI、Worker 等不同形态