电视剧《180天重启计划》剧情梗概
2026-07-24 3421286
2026-07-24 0
这是一个系列 Blog,作者将以一个 PHP 全栈工程师的身份,利用 AI 工具(claude code、codex、deepseek、豆包等):从零开始学习 golang 语言,并最终完成 ai-go-mall(github | gitee)开源项目的制作,全程记录分享。

在上一期,我们已经讲到必须学什么,如何快速学习,以及完成立项前准备,本期将完成 ai-go-mall 的目录结构创建、初始化 GIT、设计并开发配置系统。
ai-go-mall 目录,并执行 go mod init ai-go-mall 命令完成初始化项目目录结构.md 和 编码风格最佳实践.md 放入 /doc 目录ai-go-mall 目录,执行 claude 命令,直接让它:根据 @doc项目目录结构.md 创建需要的文件夹review。.├── go.mod├── .claude/│ └── settings.local.json├── cmd/│ └── api/├── config/├── doc/│ ├── 项目目录结构.md│ └── 编码风格最佳实践.md├── internal/│ ├── handler/│ ├── model/│ ├── repository/│ └── service/├── pkg/│ ├── logger/│ └── validator/└── script/
pkg 和 script,同时创建缺少的 cmd/api/main.go 文件git 是全球领先的分布式版本控制系统,程序员必备,不会的快去学,很简单。
git initgit config user.name "yang"git config user.email "[email protected]"
如果不需要上传至远程仓库备份的话,name 和 email 可以随便写,接下来要求 claude:帮我建立 .gitignore 文件,忽略 .DS_Store、.css.map、.local.* 等通常需要忽略的文件。
作者这边生成的 .gitignore 文件内容如下:
# OS.DS_StoreThumbs.db# IDE.idea/.vscode/*.swp*.swo# Local config*.local.*.env.*# Go/bin//vendor/*.exe*.test*.out# Frontend*.css.map*.js.mapnode_modules//dist/# Build output/tmp//log/
作者手动将 *.swo、*.swp 改为了 *.sw?,去掉了 .env
经过多方对比,决定选择 Viper 管理配置,它是适用于 Go 应用程序的完整配置解决方案,github 上有 30k star,非常完善和通用。
规划配置系统(直接复制以下规划,让 cc 帮助实现):
应用名称、开发服务端口号、数据库相关 配置项,放在一个配置文件内即可AI 工作完成后,我们得到以下输出:
# 大模型是概率生成,不是固定查表,再加上采样策略、上下文、版本等影响,同问题输出都会有差异,以下 AI 输出、包括生成的代码仅供参考,可直接忽略,继续向下查看如何寻找问题和完成整改配置系统已实现完毕。以下是最终文件结构和验证结果:config/├── config.yaml # 主配置文件(Git 跟踪)├── .env.yaml # 本地环境配置(Git 忽略)└── .env.yaml.example # 环境配置示例(Git 跟踪)pkg/config/└── config.go # 配置加载逻辑cmd/api/└── main.go # 入口,调用配置系统核心设计:- config.Load("config") 自动扫描 /config 目录下所有 .yaml 文件,按文件名排序后逐个 Merge,.env.yaml 排最后加载,值覆盖前面的文件- 新增配置文件无需改代码,放入 /config 目录即可- 数据库默认类型为 PostgreSQL,连接池默认值 max_open=25 / max_idle=10 / max_lifetime=5m- 提供 ConnMaxLifetimeDuration() 方法将字符串转为 time.Duration验证结果:┌─────────────────┬─────────────┬─────────────────────┐│ 配置项│ config.yaml │ .env.yaml(覆盖后) │├─────────────────┼─────────────┼─────────────────────┤│ app.name│ ai-go-mall│ ai-go-mall│├─────────────────┼─────────────┼─────────────────────┤│ server.port │ 8080│ 8080│├─────────────────┼─────────────┼─────────────────────┤│ database.host │ localhost │ 192.168.1.100 │├─────────────────┼─────────────┼─────────────────────┤│ database.port │ 5432│ 5433│├─────────────────┼─────────────┼─────────────────────┤│ database.dbname │ ai-go-mall│ ai-go-mall-dev│└─────────────────┴─────────────┴─────────────────────┘✻ Churned for 2m 50s
检查结果,从目录结构的修改看起,发现一些问题:
.env.yaml 和 .env.yaml.example 放入了 /config 目录,这与预期不符,其实我的本意是希望环境配置直接在根目录pkg/config.go,这虽然很合理,但是我更希望配置加载直接在 /config 目录以内,即最终得到 /config/config.go + 多个 .yaml 文件 的目录结构(未来会迁移到 internal 目录,最终 /config 只存储 yaml 文件)。将以上发现直接发给 cc,让它继续修改,由于目录结构错误,暂时未对配置加载的代码进行 review:
# 大模型是概率生成,不是固定查表,再加上采样策略、上下文、版本等影响,同问题输出都会有差异,以下 AI 输出、包括生成的代码仅供参考,可直接忽略,继续向下查看如何寻找问题和完成整改三个问题已全部修正,最终结构如下:.├── .env.yaml# 根目录,Git 忽略 ✓├── .env.yaml.example# 根目录,Git 跟踪,含全部配置项 ✓├── .gitignore├── config/│ ├── config.go# 配置加载逻辑 ✓│ └── config.yaml# 主配置文件└── cmd/api/main.go改动要点:┌──────────────────────────────────┬───────────────────────────────────────────────────────────┐│ 问题 │ 修正│├──────────────────────────────────┼───────────────────────────────────────────────────────────┤│ .env.yaml 放入 /config │ 移至根目录│├──────────────────────────────────┼───────────────────────────────────────────────────────────┤│ config.go 放入 /pkg/config │ 移至 /config/config.go,import 路径变为 ai-go-mall/config │├──────────────────────────────────┼───────────────────────────────────────────────────────────┤│ .env.yaml.example 仅含数据库配置 │ 补充了 app.name、server.port 及数据库全套配置 │└──────────────────────────────────┴───────────────────────────────────────────────────────────┘加载顺序: config/*.yaml → 根目录 .env.yaml(最后加载,覆盖前面的值)。.env.yaml 不存在时优雅降级,不报错。✻ Worked for 1m 24s
接下来的工作就非常重要了,为了学习和商业级项目的目标,我们需要逐行理解代码,并找出代码中的不足,或者就特意的做一些自己更加喜欢的修改,作者找到的问题如下:
timezone 和 prefix(历往项目习惯有个 prefix 配置项,可以不用,但可以有)config.Get 函数,用于获取配置项(现在是 cfg.App.Name,改为 config.Get().App.Name),前者配置可以被外部修改,且包装一个方法扩展性总是更高一些的viper.AutomaticEnv()(非加载项目的 .env.yaml 环境配置文件),这种环境变量可以通过以下方式动态设定:# Linux / MacSERVER_PORT=8080 APP_NAME=myapp go run main.go# Windows CMDset SERVER_PORT=8080&& set APP_NAME=myapp&& go run main.go# Windows PowerShell$env:SERVER_PORT="8080"; $env:APP_NAME="myapp"; go run main.go
动态设定的 APP_NAME 会覆盖到 app.name 配置项,cc 还修改了 go.mod 和 .gitignore 等文件,其中对 .gitignore 的修改如下:
# .gitignore- .env.*+ .env.yaml+ !.env.yaml.example
修改很合理,但是 + !.env.yaml.example 是多余的,直接删除即可。
继续要求 AI 按需求进行修改,再经过几轮对话之后,配置系统开发完成,完整代码开源于:github | gitee,核心代码如下:
c.ZeroFields = true 选项// main.gopackage mainimport ("fmt""log""ai-go-mall/config")func main() {// 初始化配置if err := config.Init(); err != nil {log.Fatalf("config init: %v", err)}cfg := config.Get()fmt.Printf("应用名称: %sn", cfg.App.Name)fmt.Printf("服务端口: %dn", cfg.Server.Port)fmt.Printf("数据库类型: %sn", cfg.Database.Type)fmt.Printf("数据库地址: %s:%dn", cfg.Database.Host, cfg.Database.Port)fmt.Printf("数据库名称: %sn", cfg.Database.DBName)fmt.Printf("连接池配置: max_open=%d max_idle=%d max_lifetime=%dn",cfg.Database.MaxOpenConns,cfg.Database.MaxIdleConns,cfg.Database.ConnMaxLifetime,)}
app:name: ai-go-mallserver:port: 8080database:type: postgreshost: 127.0.0.1port: 5432user: postgrespassword: "123456"dbname: aigoprefix: sslmode: disabletimezone: Asia/Shanghaimax_open_conns: 25max_idle_conns: 10conn_max_lifetime: 300 # 秒
// config.gopackage configimport ("fmt""log""os""path/filepath""sync""github.com/fsnotify/fsnotify""github.com/go-viper/mapstructure/v2""github.com/spf13/viper")// App 应用配置type App struct {Name string}// Server 服务配置type Server struct {Port int}// Database 数据库配置type Database struct {TypestringHoststringPortintUserstringPasswordstringPrefixstringDBNamestring `mapstructure:"dbname"`SSLMode string `mapstructure:"sslmode"`Timezonestring `mapstructure:"timezone"`MaxOpenConnsint`mapstructure:"max_open_conns"`MaxIdleConnsint`mapstructure:"max_idle_conns"`ConnMaxLifetime int`mapstructure:"conn_max_lifetime"`}// Config 聚合配置type Config struct {AppAppServer ServerDatabase Database}var (vip*viper.Viperconfig = new(Config)mu sync.RWMutex)// 初始化配置func Init() error {if vip == nil {vip = viper.New()// 读取 config 目录下所有 yaml 文件files, err := filepath.Glob(filepath.Join("./config", "*.yaml"))if err != nil {return fmt.Errorf("get config files: %w", err)}for _, file := range files {vip.SetConfigFile(file)if err := vip.MergeInConfig(); err != nil {return fmt.Errorf("merge config file %s: %w", file, err)}}// 加载根目录 .env.yaml 文件(覆盖从 config 文件夹读取的配置)if _, err := os.Stat(".env.yaml"); err == nil {vip.SetConfigFile(".env.yaml")if err := vip.MergeInConfig(); err != nil {return fmt.Errorf("merge config file .env.yaml: %w", err)}}// 支持环境变量覆盖vip.AutomaticEnv()// 解析到结构体if err := vip.Unmarshal(config); err != nil {return fmt.Errorf("unmarshal config: %w", err)}// 监听配置改变vip.OnConfigChange(func(e fsnotify.Event) {mu.Lock()defer mu.Unlock()if err := vip.Unmarshal(config, func(c *mapstructure.DecoderConfig) {c.ZeroFields = true}); err != nil {log.Printf("config reload failed: %v", err)}})vip.WatchConfig()}return nil}// 返回全局配置副本。func Get() Config {mu.RLock()defer mu.RUnlock()return *config}// 获取 Viper 全局单例func GetViper() *viper.Viper {return vip}// 是否已经初始化func IsInit() bool {return vip != nil}