390 lines
9.2 KiB
Markdown
390 lines
9.2 KiB
Markdown
# 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)
|
||
|
||
```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)
|
||
|
||
```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)
|
||
|
||
```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)
|
||
|
||
```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. **多种运行模式**
|
||
|
||
```bash
|
||
# 启动 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
|
||
# 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
|
||
# 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 等不同形态
|