3. 项目初始化与工程结构:使用 Go Module 初始化项目、标准目录结构设计(cmd/internal/pkg)、配置文件管理
一个支付中间件的工程落地,始于清晰的项目初始化与严谨的目录结构。本章将带领你使用 Go Module 完成项目初始化,并设计一套符合支付场景的标准目录结构,同时解决配置文件管理的核心问题。这是后续所有开发工作的基石。
3.1 使用 Go Module 初始化项目
Go Module 是 Go 1.11 版本引入的官方依赖管理方案,已成为 Go 项目的标准。对于支付中间件这种需要长期维护、多模块协作的项目,必须从一开始就正确使用 Module。
3.1.1 初始化命令与模块路径
在项目根目录下执行以下命令:
mkdir go-payment-middleware
cd go-payment-middleware
go mod init github.com/your-org/go-payment-middleware
模块路径规范: 模块路径建议使用完整的仓库地址(如
github.com/xxx/xxx),这能避免后续引入内部包时的路径冲突。支付项目通常涉及多个子模块,清晰的路径前缀有助于代码复用。
3.1.2 依赖管理与版本锁定
支付中间件通常依赖以下核心库:
| 依赖库 | 用途 | 版本建议 |
|---|---|---|
github.com/gin-gonic/gin |
HTTP 路由与中间件框架 | v1.9.x |
github.com/go-redis/redis/v9 |
分布式缓存与幂等性支持 | v9.x |
github.com/spf13/viper |
多源配置文件管理 | v1.18.x |
go.uber.org/zap |
高性能结构化日志 | v1.26.x |
github.com/go-sql-driver/mysql |
MySQL 数据库驱动 | v1.7.x |
添加依赖并生成 go.sum 锁定文件:
go get github.com/gin-gonic/gin@v1.9.1
go get github.com/spf13/viper@v1.18.2
go mod tidy
版本锁定原则: 支付系统对稳定性要求极高,
go.sum 文件必须纳入版本控制。禁止使用 latest 或 master 分支作为依赖版本,必须锁定到具体的语义化版本号。
3.2 标准目录结构设计(cmd/internal/pkg)
支付中间件的目录结构需要兼顾 可扩展性、模块隔离 和 部署便利性。我们采用 Go 社区推荐的 cmd/internal/pkg 三层架构,并针对支付场景做定制优化。
3.2.1 顶层目录结构总览
go-payment-middleware/
├── cmd/ # 可执行入口
│ ├── api/ # API 服务入口
│ │ └── main.go
│ └── worker/ # 异步任务入口
│ └── main.go
├── internal/ # 私有应用代码
│ ├── config/ # 配置加载与解析
│ ├── handler/ # HTTP 请求处理器
│ ├── middleware/ # 支付专用中间件
│ ├── model/ # 数据模型与实体
│ ├── repository/ # 数据访问层
│ ├── service/ # 业务逻辑层
│ └── router/ # 路由注册
├── pkg/ # 可复用的公共库
│ ├── crypto/ # 加密签名工具
│ ├── idempotent/ # 幂等性校验
│ ├── notify/ # 通知渠道封装
│ └── retry/ # 重试策略
├── configs/ # 配置文件模板
│ ├── config.yaml
│ └── config.dev.yaml
├── scripts/ # 构建与部署脚本
├── docs/ # 项目文档
├── go.mod
└── go.sum
3.2.2 各目录职责与设计原则
| 目录 | 职责 | 关键约束 |
|---|---|---|
cmd/ |
每个子目录对应一个可独立部署的二进制 | 只包含 main.go,代码量不超过 50 行 |
internal/ |
业务核心逻辑,不允许外部包导入 | Go 编译器强制隔离,防止滥用 |
pkg/ |
通用工具库,可被其他项目引用 | 必须无业务耦合,只提供基础能力 |
configs/ |
环境配置模板,不包含敏感信息 | 敏感字段使用占位符,通过环境变量注入 |
支付项目特别提醒:
internal/ 目录下的 middleware/ 专门存放支付链路特有的中间件,如签名校验、幂等拦截、商户鉴权等。这些中间件不应放在 pkg/ 中,避免被外部滥用。
3.2.3 目录结构对应的依赖关系
下图展示了各层之间的依赖方向,遵循 单向依赖 原则:
3.3 配置文件管理
支付中间件通常需要管理多套环境(开发、测试、预发布、生产)的配置,且涉及敏感信息(密钥、证书路径、数据库密码)。我们采用 Viper + 多源加载 策略。
3.3.1 配置文件分层
配置文件按 默认值 → 环境特定 → 环境变量 三层覆盖:
configs/
├── config.yaml # 默认配置,所有环境共享
├── config.dev.yaml # 开发环境覆盖
├── config.staging.yaml # 预发布环境覆盖
└── config.prod.yaml # 生产环境覆盖
3.3.2 配置加载核心代码
在 internal/config 中实现统一的配置加载器:
package config
import (
"fmt"
"github.com/spf13/viper"
)
type Config struct {
Server ServerConfig `mapstructure:"server"`
Database DatabaseConfig `mapstructure:"database"`
Redis RedisConfig `mapstructure:"redis"`
Payment PaymentConfig `mapstructure:"payment"`
}
type ServerConfig struct {
Port int `mapstructure:"port"`
Mode string `mapstructure:"mode"` // debug, release, test
}
type DatabaseConfig struct {
DSN string `mapstructure:"dsn"`
}
type RedisConfig struct {
Addr string `mapstructure:"addr"`
Password string `mapstructure:"password"`
DB int `mapstructure:"db"`
}
type PaymentConfig struct {
SignKey string `mapstructure:"sign_key"`
NotifyURL string `mapstructure:"notify_url"`
RetryTimes int `mapstructure:"retry_times"`
}
func LoadConfig(env string) (*Config, error) {
v := viper.New()
v.SetConfigName("config")
v.SetConfigType("yaml")
v.AddConfigPath("./configs")
// 1. 加载默认配置
if err := v.ReadInConfig(); err != nil {
return nil, fmt.Errorf("read default config: %w", err)
}
// 2. 加载环境特定配置(覆盖)
if env != "" {
envConfigName := fmt.Sprintf("config.%s", env)
v.SetConfigName(envConfigName)
if err := v.MergeInConfig(); err != nil {
// 环境配置文件不存在时不报错,仅使用默认
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
return nil, fmt.Errorf("merge env config: %w", err)
}
}
}
// 3. 环境变量覆盖(最高优先级)
v.AutomaticEnv()
v.SetEnvPrefix("PAY")
var cfg Config
if err := v.Unmarshal(&cfg); err != nil {
return nil, fmt.Errorf("unmarshal config: %w", err)
}
return &cfg, nil
}
3.3.3 环境变量命名规范
支付系统敏感信息必须通过环境变量注入,禁止硬编码在配置文件中。命名规则如下:
| 配置项 | 环境变量名 | 示例值 |
|---|---|---|
| 数据库 DSN | PAY_DATABASE_DSN |
user:pass@tcp(host:3306)/db?charset=utf8 |
| Redis 密码 | PAY_REDIS_PASSWORD |
your-redis-password |
| 支付签名密钥 | PAY_PAYMENT_SIGN_KEY |
sk_live_xxxxxxxxxxxx |
| 服务端口 | PAY_SERVER_PORT |
8080 |
环境变量前缀: 使用
PAY_ 作为统一前缀,避免与系统或其他应用的环境变量冲突。Viper 的 SetEnvPrefix 会自动将 PAY_DATABASE_DSN 映射到配置中的 database.dsn。
3.3.4 配置加载优先级
下图展示了配置加载的完整流程与覆盖顺序:
3.4 项目初始化完整流程
将以上所有步骤整合为一个可执行的初始化流程:
- 创建项目根目录 并执行
go mod init - 创建标准目录结构:
cmd/api、cmd/worker、internal/、pkg/、configs/ - 编写默认配置文件
configs/config.yaml,包含所有非敏感字段的默认值 - 实现配置加载器
internal/config/config.go,支持三层覆盖 - 编写入口文件
cmd/api/main.go,调用配置加载并启动服务 - 执行
go mod tidy整理依赖,生成go.sum - 验证编译:
go build ./cmd/api/确保无错误
工程落地检查清单:
- ✅
go.mod中模块路径与仓库地址一致 - ✅
internal/目录下无对外暴露的导出函数 - ✅
pkg/目录下的包不依赖internal/ - ✅ 配置文件中的敏感字段使用
${ENV_VAR}占位符 - ✅
.gitignore包含config.prod.yaml和.env - ✅ 环境变量前缀统一为
PAY_