位置:首页 > 进阶教程 > 程序员转型手记:PHP到AI+Golang目录与配置系统开发

程序员转型手记:PHP到AI+Golang目录与配置系统开发

时间:2026-07-24  |  作者:游戏探长  |  阅读:0
这是一个系列 Blog,作者将以一个 PHP 全栈工程师的身份,利用 AI 工具(claude code、codex、deepseek、豆包等):从零开始学习 golang 语言,并最终完成 ai-go-mall(github | gitee)开源项目的制作,全程记录分享。

从 PHP 到 AI + Golang,程序员自救转型手记(二):目录结构、初始化 GIT、设计并开发配置系统

上期聊了必须学什么、怎么快速上手,以及立项前的准备工作。这期直接动手,把 ai-go-mall 的目录结构搞出来,顺便初始化 Git,再设计并开发一套配置系统。

目录结构

首先创建 ai-go-mall 目录,然后执行 go mod init ai-go-mall 完成初始化,这是 Go 项目的基础操作。 把之前准备好的 项目目录结构.md编码风格最佳实践.md 放进 /doc 目录。接着切到 ai-go-mall 目录,执行 claude 命令,让它根据 @doc项目目录结构.md 创建需要的文件夹。 AI 申请授权时,只要不涉及越权执行命令、安装盗版依赖,或者使用不靠谱的中转站(防止中间人攻击),一般都可以同意。不过话说回来,最后每一行代码还得咱们自己过一遍。
.
├── go.mod
├── .claude/
│   └── settings.local.json
├── cmd/
│   └── api/
├── config/
├── doc/
│   ├── 项目目录结构.md
│   └── 编码风格最佳实践.md
├── internal/
│   ├── handler/
│   ├── model/
│   ├── repository/
│   └── service/
├── pkg/
│   ├── logger/
│   └── validator/
└── script/
生成完发现,有些目录短期内用不上,比如 pkgscript,直接删掉。同时手动创建 cmd/api/main.go 文件,这是入口文件,必不可少。

初始化 GIT

Git 是全世界最流行的版本控制系统,没什么好说的,必备技能,不会的去学一下,很快就能上手。
git init
git config user.name "yang"
git config user.email "1094963513@qq.com"
如果不上传远程仓库,名字和邮箱随便写就行。接下来让 Claude 帮忙建个 .gitignore 文件,把 .DS_Store*.css.map*.local.* 这类常见忽略项都加进去。 生成的 .gitignore 内容如下:
# OS
.DS_Store
Thumbs.db
# IDE
.idea/
.vscode/
*.swp
*.swo
# Local config
*.local.*
.env
# Go
/bin/
/vendor/
*.exe
*.test
*.out
# Frontend
*.css.map
*.js.map
node_modules/
/dist/
# Build output
/tmp/
/log/
手动把 *.swo*.swp 改成 *.sw,同时去掉了 .env 这条规则,因为后面会用到 .env.yaml,不能一起忽略。

设计并开发配置系统

对比了几种方案,最终选了 Viper 来管理配置。它是 Go 生态里最完整的配置解决方案,GitHub 上 30k 的 star 数就是最好的背书。 规划配置系统的核心思路如下: - 用 github.com/spf13/viper 管理配置 - 初版包含 应用名称、服务端口号、数据库相关 配置项,一个配置文件搞定 - 所有配置文件用 YAML 格式,存放在 /config 目录下,配置系统能自动加载所有 YAML 文件,新增配置不需要改代码 - 额外加一个 .env.yaml 环境配置文件,优先级最高,用于开发环境自定义数据库配置,Git 需要忽略 - 再配一个 .env.yaml.example 文件,Git 不忽略,方便开发者快速创建自己的环境配置 - 数据库配置带上 type 字段,默认用 PostgreSQL,同时准备好连接池配置项 把这个规划扔给 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
检查一下目录结构,发现几个问题: 1. AI 把 .env.yaml.env.yaml.example 放在了 /config 目录下。其实本意是让环境配置直接放在根目录。 2. 配置加载逻辑放在了 pkg/config.go,虽然合理,但更希望配置加载直接在 /config 目录下,最终结构是 /config/config.go + 多个 .yaml 文件。未来会迁到 internal 目录,那时 /config 只存 YAML 文件。 3. .env.yaml 里只有数据库配置,没有 server.portapp.name 配置项。 把这些问题发给 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
接下来是关键步骤——逐行理解代码,找出不足,或者按照自己的偏好做调整。找到的问题如下: 1. config.Load 函数需要传递 config 目录路径,但当前项目直接用 /config 目录即可,没必要传参。 2. config.Load 有“加载”的意思,在 main.go 调用,但改为 config.Init 函数更合理。 3. 没加载到任何配置文件时,不需要报错。 4. 数据库配置缺少 timezoneprefix(往期项目习惯加个 prefix 配置项,可以不用,但不能没有)。 5. 不需要对数据库配置做任何预处理,后续在连接数据库的函数里处理。 6. 研究后,要求 AI 提供一个 config.Get 函数,用于获取配置项(现在是 cfg.App.Name,改为 config.Get().App.Name),前者配置可能被外部修改,包装一个方法扩展性更好。 7. 对环境变量进行加载,并覆盖已有配置项,即调用 viper.AutomaticEnv()(不是加载项目的 .env.yaml 环境配置文件)。环境变量可以通过以下方式动态设定:
# Linux / Mac
SERVER_PORT=8080 APP_NAME=myapp go run main.go
# Windows CMD
set 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 配置项。AI 还修改了 go.mod.gitignore 等文件,其中 .gitignore 的修改如下:
# .gitignore
- .env.*
+ .env.yaml
+ !.env.yaml.example
修改很合理,但 + !.env.yaml.example 是多余的,直接删除即可。 继续让 AI 按需求修改,经过几轮对话后,配置系统开发完成。完整代码开源于:github | gitee,核心代码如下: 1. 让 AI 增加了监听配置改变,以及配置数据的读写锁。 2. 监听配置改变时,当新配置为空可能不会覆盖,研究后添加了 c.ZeroFields = true 选项。 3. 同时确认了:结构体定义代码在最上面,是 Golang 项目的通用做法。
// main.go
package main

import (
    "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-mall
server:
  port: 8080
database:
  type: postgres
  host: 127.0.0.1
  port: 5432
  user: postgres
  password: "123456"
  dbname: aigo
  prefix:
  sslmode: disable
  timezone: Asia/Shanghai
  max_open_conns: 25
  max_idle_conns: 10
  conn_max_lifetime: 300 # 秒
// config.go
package config

import (
    "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 {
    Type            string
    Host            string
    Port            int
    User            string
    Password        string
    Prefix          string
    DBName          string `mapstructure:"dbname"`
    SSLMode         string `mapstructure:"sslmode"`
    Timezone        string `mapstructure:"timezone"`
    MaxOpenConns    int    `mapstructure:"max_open_conns"`
    MaxIdleConns    int    `mapstructure:"max_idle_conns"`
    ConnMaxLifetime int    `mapstructure:"conn_max_lifetime"`
}

// Config 聚合配置
type Config struct {
    App      App
    Server   Server
    Database Database
}

var (
    vip    *viper.Viper
    config = 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
}

免责声明:文中图文均来自网络,如有侵权请联系删除,心愿游戏发布此文仅为传递信息,不代表心愿游戏认同其观点或证实其描述。

相关文章

更多

精选合集

更多

大家都在玩

热门话题

大家都在看

更多