# Phase 1: 项目骨架 & 基础设施 — 详细计划 > **所属主计划**: [main-plan.md](main-plan.md#phase-1-项目骨架--基础设施第-1-2-天) > **预计时间**: 第 1-2 天 > **核心学习**: Go 项目组织、package 管理、Gin 基础 --- ## 学习目标 | Go 概念 | 说明 | Java 对照 | |----------|------|-----------| | `go mod init` / `go mod tidy` | 依赖管理 | Maven `pom.xml` / Gradle `build.gradle`,但更轻量 | | package 组织 | 按功能分目录,每个目录一个 package | Java package 按目录组织 | | `func main()` | 程序入口 | `public static void main(String[])` | | `init()` | 包初始化函数,main 之前自动执行 | Spring `@PostConstruct` / static initializer | | pointer `*` | 指针类型,传递引用而非拷贝 | Java 中除了基本类型,对象默认是引用 | | value `&` | 取地址 | Java 无直接对应 | | `defer` | 函数返回前执行,常用于清理资源 | `try-finally` 的 `finally` 块 | | error 返回值 | 函数返回 error,调用方检查 | Java checked exception,但更显式 | | `:=` vs `var` | 短声明 vs 显式声明 | `var` 类型推断 | | struct tag | 结构体字段上的元数据注解 | Java annotation(`@JsonProperty` 等) | --- ## 任务分解 ### Step 1.0: 初始化 Go Module **任务**: 在项目根目录执行 `go mod init`,建立 Go 模块。 **关键点**: - 模块路径已经在 CLAUDE.md 中定义为 `synoth.com/janus` - 不要用 `github.com/xxx` —— 这是学习项目,不需要托管到 GitHub - `go.mod` 相当于 Java Maven 的 `pom.xml`,记录模块名、Go 版本、依赖 **验证**: `cat go.mod` 确认模块声明正确 --- ### Step 1.1: 安装核心依赖 **任务**: 通过 `go get` 安装以下依赖包: | 包 | 用途 | Java 类比 | |----|------|-----------| | `github.com/gin-gonic/gin` | Web 框架,路由 + 中间件 | Spring MVC | | `gorm.io/gorm` | ORM 核心库 | JPA / Hibernate | | `gorm.io/driver/postgres` | GORM PostgreSQL 驱动 | PostgreSQL JDBC Driver | | `github.com/spf13/viper` | 配置管理(YAML + 环境变量) | Spring `@ConfigurationProperties` | | `github.com/joho/godotenv` | 加载 `.env` 文件到环境变量 | dotenv-java | **关键点**: - `go get` 会自动更新 `go.mod` 和生成 `go.sum`(类似 `pom.xml` + 依赖锁) - `go.sum` 记录每个依赖的校验和,确保可复现构建 - 间接依赖(indirect)会被自动标记 **验证**: `go.mod` 中出现所有依赖,`go.sum` 非空 --- ### Step 1.2: 实现配置加载 — `internal/config/config.go` **任务**: 用 Viper 加载 `config.yaml` + 环境变量覆盖。 **需要定义的结构体**: ```go // Config —— 顶层配置结构体 type Config struct { Server ServerConfig `mapstructure:"server"` Database DatabaseConfig `mapstructure:"database"` } // ServerConfig —— HTTP 服务器配置 type ServerConfig struct { Port int `mapstructure:"port"` // 默认 8080 Mode string `mapstructure:"mode"` // debug / release / test } // DatabaseConfig —— PostgreSQL 连接配置 type DatabaseConfig struct { Host string `mapstructure:"host"` Port int `mapstructure:"port"` User string `mapstructure:"user"` Password string `mapstructure:"password"` DBName string `mapstructure:"dbname"` SSLMode string `mapstructure:"sslmode"` } ``` **需要实现的函数**: - `Load(path string) (*Config, error)` — 加载配置文件 - 支持环境变量覆盖(如 `DATABASE_PASSWORD` 覆盖 YAML 中的明文密码) **Go 学习点**: - `mapstructure` tag 是 Viper 专用的 struct tag,类似 Java `@ConfigurationProperties(prefix="...")` - `*Config` 返回指针而非值 — Go 中大型 struct 传指针避免拷贝 - error 作为返回值,调用方必须处理 **Viper 配置方式**: ```go v := viper.New() v.SetConfigFile(path) // 指定配置文件路径 v.AutomaticEnv() // 自动读取环境变量 v.SetEnvKeyReplacer(...) // 将 "." 替换为 "_"(database.host → DATABASE_HOST) ``` **验证**: 写一个简单的测试(可选),或者直接在 main 中调用 Load 打印结果 --- ### Step 1.3: 编写配置文件 — `config.yaml` **任务**: 在项目根目录创建默认配置文件。 **内容要点**: ```yaml server: port: 8080 mode: debug # Gin 模式:debug(带日志)/ release(生产)/ test database: host: localhost port: 5432 user: janus password: "" # 留空,通过环境变量或 .env 传入 dbname: janus sslmode: disable # 本地开发关 SSL,生产用 require ``` **Go 学习点**: - YAML 用缩进表示层级,没有 XML/JSON 的括号 - Viper 默认支持 YAML、JSON、TOML 等格式 - 敏感信息(密码)不写死在 YAML 中,通过环境变量注入 **验证**: 文件语法正确(可以用在线 YAML 验证器或 `yamllint`) --- ### Step 1.4: 添加 `.env` 支持 **任务**: 创建 `.env.example` 模板文件,并在 `main.go` 中用 `godotenv.Load()` 加载。 **`.env.example` 内容**: ``` DATABASE_PASSWORD=your_password_here ``` **关键点**: - `.env` 加入 `.gitignore`,避免敏感信息提交 - `.env.example` 提交到仓库,作为模板 - `godotenv.Load()` 在 `main()` 最开始调用,失败不阻断启动(`.env` 可能不存在) **Go 学习点**: - `godotenv.Load()` 读取 `.env` 并设置到进程环境变量 - Viper 的 `AutomaticEnv()` 自动读取环境变量,两者配合工作 **验证**: 创建 `.env` 文件后,`os.Getenv("DATABASE_PASSWORD")` 能读到值 --- ### Step 1.5: 实现 GORM 数据库连接 **任务**: 在 `internal/config/` 或单独的 `internal/database/` 中实现 DB 连接初始化。 **需要实现的函数**: ```go func NewDatabase(cfg *DatabaseConfig) (*gorm.DB, error) ``` **实现要点**: 1. 构造 PostgreSQL DSN(Data Source Name): ``` host=localhost user=janus password=xxx dbname=janus port=5432 sslmode=disable ``` 2. `gorm.Open(postgres.Open(dsn), &gorm.Config{})` 打开连接 3. 获取底层 `*sql.DB`,配置连接池参数: - `SetMaxOpenConns(25)` — 最大打开连接数 - `SetMaxIdleConns(10)` — 最大空闲连接数 - `SetConnMaxLifetime(5 * time.Minute)` — 连接最大存活时间 4. `db.Ping()` 验证连接是否可用 **Go 学习点**: - GORM 的 `gorm.Open()` 返回 `(*gorm.DB, error)`,惯例返回 error - `*sql.DB` 是 Go 标准库的数据库句柄,GORM 在其上构建 - 连接池配置对标 Java HikariCP 的 `maximumPoolSize`、`minimumIdle`、`maxLifetime` - `defer` 不在这里用(连接需要在整个应用生命周期保持),而是在 `main.go` 中管理 **验证**: 启动程序,GORM 打印连接日志无报错 --- ### Step 1.6: 实现健康检查 Handler — `internal/handler/health_handler.go` **任务**: 实现一个简单的健康检查端点。 **代码结构**: ```go package handler import "github.com/gin-gonic/gin" type HealthHandler struct { db *gorm.DB // 用于检查数据库连接 } func NewHealthHandler(db *gorm.DB) *HealthHandler { ... } func (h *HealthHandler) Check(c *gin.Context) { // 1. ping 数据库 // 2. 返回 JSON: { "status": "ok", "db": "connected" } } ``` **返回格式**: ```json { "status": "ok", "timestamp": "2026-07-08T12:00:00Z", "db": "connected" } ``` **Go 学习点**: - `(h *HealthHandler)` 是指针接收者(pointer receiver)— 方法可以修改 h 的状态 - `gin.Context` 封装了 HTTP 请求和响应,类似 Spring 的 `HttpServletRequest` + `HttpServletResponse` - `c.JSON(200, obj)` 自动设置 Content-Type 并序列化 JSON - Gin 的 handler 签名是 `func(c *gin.Context)`,不使用返回值 — 通过 `c` 写响应 **验证**: `curl http://localhost:8080/health` 返回 JSON --- ### Step 1.7: 实现路由注册 — `internal/index/index.go` **任务**: 集中管理所有路由注册。 **代码结构**: ```go package index func Setup(r *gin.Engine, handler *handler.HealthHandler) { r.GET("/health", handler.Check) } ``` **Go 学习点**: - 函数接收 `*gin.Engine`(Gin 的路由引擎),在其上注册路由 - 这是"依赖注入"的最简形式 —— 手动传参,不用框架的 DI 容器 - 路由分离到单独文件,避免 `main.go` 膨胀 **验证**: 路由文件编译通过,程序运行后 `/health` 可访问 --- ### Step 1.8: 实现程序入口 — `cmd/server/main.go` **任务**: 组装所有依赖,启动 HTTP 服务器。 **流程**: ``` main() ├── godotenv.Load(".env") // 加载环境变量 ├── config.Load("config.yaml") // 加载配置 ├── database.NewDatabase(&cfg.Database) // 初始化 DB ├── handler.NewHealthHandler(db) // 创建 handler ├── gin.New() / gin.Default() // 创建 Gin 引擎 ├── index.Setup(r, healthHandler) // 注册路由 └── r.Run(":8080") // 启动服务器 ``` **Gin 模式选择**: - `gin.Default()` — 带 Logger 和 Recovery 中间件(开发推荐) - `gin.New()` — 空白引擎,手动添加中间件 **Go 学习点**: - `main` 函数必须在 `package main` 中,否则无法编译成可执行文件 - `main()` 无参数、无返回值 — 退出用 `os.Exit(code)` - `defer` 用于在 main 返回前关闭数据库连接等资源 - 包的 `init()` 函数在 main 之前自动执行(GORM 驱动注册就是通过 init) **验证**: `go run ./cmd/server/main.go` 启动成功,访问 `http://localhost:8080/health` --- ### Step 1.9: 创建 `.gitignore` **任务**: 确保敏感文件和构建产物不提交。 **必须忽略的内容**: ``` # 环境变量 .env # 构建产物 /server /janus *.exe # IDE .idea/ .vscode/ *.swp # 依赖 vendor/ # 临时文件 tmp/ temp/ ``` --- ## 文件清单(Phase 1 产出) ``` janus/ ├── cmd/server/main.go ← Step 1.8 实现 ├── internal/ │ ├── config/ │ │ └── config.go ← Step 1.2 实现 │ ├── handler/ │ │ └── health_handler.go ← Step 1.6 实现 │ └── index/ │ └── index.go ← Step 1.7 实现 ├── config.yaml ← Step 1.3 编写 ├── .env.example ← Step 1.4 编写 ├── .gitignore ← Step 1.9 编写 ├── go.mod ← Step 1.0 生成 └── go.sum ← Step 1.1 生成 ``` --- ## Phase 1 完成标准 - [ ] `go build ./...` 编译通过,无错误 - [ ] `go vet ./...` 静态分析无警告 - [ ] 程序启动后 `curl http://localhost:8080/health` 返回 `{"status":"ok",...}` - [ ] GORM 成功连接 PostgreSQL(或优雅报错"数据库未启动"而不 panic) - [ ] 配置文件中的值被正确读取(改端口后服务在新端口启动) - [ ] `.env` 中的密码能覆盖 `config.yaml` 中的空值 --- ## 常见问题 & 排错 | 问题 | 可能原因 | 解决 | |------|----------|------| | `go get` 失败/慢 | 网络问题 | 设置 `GOPROXY=https://goproxy.cn,direct` | | `cannot find package` | 模块路径不对 | 检查 `go.mod` 的 module 名和 import 路径一致 | | GORM 连接报错 | PostgreSQL 未启动 | `pg_isready` 检查,或用 Docker 启动 PostgreSQL | | `import cycle not allowed` | 循环依赖 | Go 禁止包之间循环引用,检查 import 关系 | | Viper 读不到配置 | 路径问题 | 用绝对路径或相对于执行目录的路径 | | `:=` vs `=` 报错 | 短声明只能用于新变量 | 已声明的变量用 `=` 赋值 | --- ## 关键设计决策 1. **手动依赖注入** — 不使用 wire/di 框架。Phase 1 依赖少,手动传参最清晰,也是 Go 社区的常见做法 2. **Config 用指针返回** — `func Load() (*Config, error)` 而非 `(Config, error)`,避免大 struct 拷贝 3. **health check 注入 `*gorm.DB`** — 真实检查数据库连通性,而非返回假 OK 4. **`gin.Default()` 而非 `gin.New()`** — 开发阶段带 Logger 和 Recovery 中间件更方便