feat(config): 添加配置管理模块和数据库连接功能

- 实现全局配置结构体,包含服务、数据库和日志配置
- 添加 Viper 配置加载功能,支持 YAML 文件和环境变量
- 创建 config.yaml 配置文件,包含服务器和数据库连接信息
- 集成 GORM 数据库连接功能,支持 PostgreSQL 驱动
- 更新依赖包,添加 postgres 驱动和相关工具库
- 实现 main 入口函数,加载配置并初始化服务
- 创建详细的 Phase 1 基础设施实施计划文档
This commit is contained in:
yangzhaohan
2026-07-09 08:52:29 +08:00
parent ea1dd4876d
commit 262c8377b9
8 changed files with 484 additions and 10 deletions
+8 -10
View File
@@ -275,18 +275,16 @@ type Strategy interface {
## 分阶段实施计划
### Phase 1: 项目骨架 & 基础设施(第 1-2 天)
> 📋 **详细计划**: [phase-1-infrastructure.md](phase-1-infrastructure.md)
**学习目标**: Go 项目组织、package 管理、Gin 基础
具体任务:
- [-] 初始化 `go.mod`go mod init github.com/synoth/janus
- [ ] 安装核心依赖:gin, gorm, viper, pgx 驱动
- [ ] 实现 `internal/config/config.go`Viper 加载 YAML 配置
- [ ] 编写 `config.yaml`:数据库连接、服务器端口等
- [ ] 实现 `cmd/server/main.go`:组装依赖,启动 Gin
- [ ] 实现 `internal/router/router.go`:基础路由
- [ ] 实现 `internal/handler/health_handler.go`GET /health
- [ ] 实现 GORM 数据库连接 & AutoMigrate
- [ ] 添加 `.env` 支持敏感配置
**核心任务**:
- 初始化 `go.mod`,安装 gin / gorm / viper / pgx 依赖
- 实现配置加载(Viper + YAML + 环境变量)
- 实现 GORM 数据库连接和连接池配置
- 实现 `/health` 端点,组装 Gin 启动流程
- 添加 `.env` 支持和 `.gitignore`
**交付物**: 可运行的服务,响应 `/health`,连接数据库
+366
View File
@@ -0,0 +1,366 @@
# 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 DSNData 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/router/router.go`
**任务**: 集中管理所有路由注册。
**代码结构**:
```go
package router
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 引擎
├── router.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 实现
│ └── router/
│ └── router.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 中间件更方便
+10
View File
@@ -0,0 +1,10 @@
package main
import "synoth.com/janus/internal/config"
func main() {
cfg, err := config.Load("config.yaml")
if err != nil {
panic(err)
}
}
+14
View File
@@ -0,0 +1,14 @@
server:
port: 8080
mode: debug
database:
host: 175.178.9.76
port: 25432
dbname: postgres
username: synoth
password: uKWjf4ns5ahx
sslmode: disable
log:
level: debug
+4
View File
@@ -20,8 +20,10 @@ require (
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.10.0 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/joho/godotenv v1.5.1 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
@@ -45,8 +47,10 @@ require (
golang.org/x/arch v0.22.0 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/net v0.51.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/text v0.34.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect
gorm.io/driver/postgres v1.6.0 // indirect
gorm.io/gorm v1.31.2 // indirect
)
+8
View File
@@ -35,10 +35,14 @@ github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7Ulw
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.10.0 h1:VhSvgU2jSli8o3AqIEOTJr7rZwAEUVo4E4XhR94Zfr0=
github.com/jackc/pgx/v5 v5.10.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
@@ -97,6 +101,8 @@ golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
@@ -107,5 +113,7 @@ google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
gorm.io/gorm v1.31.2 h1:3o8FXNo9v9S858gil+3LlZA1LkCOzgb4g5BL64FgaCo=
gorm.io/gorm v1.31.2/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
+64
View File
@@ -0,0 +1,64 @@
package config
import (
"fmt"
"github.com/gin-gonic/gin"
"github.com/spf13/viper"
)
// Config 全局配置
type Config struct {
Server ServerConfig `mapstructure:"server"`
Database DatabaseConfig `mapstructure:"database"`
Log LogConfig `mapstructure:"log"`
}
// ServerConfig 服务基础配置
type ServerConfig struct {
Port int `mapstructure:"port"`
Mode string `mapstructure:"mode"`
}
// DatabaseConfig 数据库配置
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"`
}
// LogConfig 日志配置
type LogConfig struct {
Level string `mapstructure:"level"`
}
func Load(path string) (*Config, error) {
v := viper.New()
v.SetConfigFile(path)
v.AutomaticEnv()
if err := v.ReadInConfig(); err != nil {
return nil, fmt.Errorf("read config: %w", err)
}
var cfg Config
if err := v.Unmarshal(&cfg); err != nil {
return nil, fmt.Errorf("unmarshal config: %w", err)
}
return &cfg, nil
}
func (c *Config) InitGinMode() {
switch c.Server.Mode {
case "release":
gin.SetMode(gin.ReleaseMode)
case "test":
gin.SetMode(gin.TestMode)
default:
gin.SetMode(gin.DebugMode)
}
}
+10
View File
@@ -0,0 +1,10 @@
package database
import "gorm.io/gorm"
type DatabaseConfig struct {
}
func Connect(cfg DatabaseConfig) (*gorm.DB, error) {
return nil, nil
}