9.2 KiB
9.2 KiB
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
目录的核心作用:
- 程序入口点 - 每个 main.go 是一个独立的应用程序
- 配置解析 - 处理命令行参数和配置文件
- 依赖注入 - 初始化和连接各个组件
- 生命周期管理 - 启动、运行、优雅关闭
- 多种运行模式 - 服务器、CLI、Worker 等不同形态