配置管理与Go服务架构
02 - 配置管理与 Go 服务架构
本章目标
- 设计一个可持久化的配置系统
- 用 Go struct + JSON 管理 API Key
- 实现 API Key 的脱敏策略
- 理解 Wails3 的 Service 注册机制
- 将 ConfigService 注册到 Wails 应用
2.1 配置设计
我们的应用需要三个配置项:
| 配置项 | 用途 | 敏感度 |
|---|---|---|
deepseek_api_key |
DeepSeek 大模型 API Key | 🔴 敏感 |
deepseek_model |
模型名称(如 deepseek-chat) |
🟢 公开 |
pexels_api_key |
Pexels 图片搜索 API Key | 🔴 敏感 |
配置文件存储在用户目录下的隐藏文件夹:
Windows: C:\Users\<用户名>\.pet-content-creator\config.json
macOS: /Users/<用户名>/.pet-content-creator/config.json
Linux: /home/<用户名>/.pet-content-creator/config.json
2.2 Go Struct 定义
创建 backend/config.go:
package backend
// Config 应用配置
type Config struct {
DeepSeekAPIKey string `json:"deepseek_api_key"`
DeepSeekModel string `json:"deepseek_model"`
PexelsAPIKey string `json:"pexels_api_key"`
}
💡 知识点:
json:"deepseek_api_key"是 Go 的 struct tag。encoding/json包会读取这些 tag 来决定 JSON 字段名。如果不写 tag,Go 默认使用大写开头的字段名。
2.3 创建 ConfigService
继续在 backend/config.go 中添加:
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
)
// ConfigService 配置管理服务(暴露给前端)
type ConfigService struct {
configPath string
}
// NewConfigService 创建配置服务
func NewConfigService() *ConfigService {
homeDir, _ := os.UserHomeDir()
configDir := filepath.Join(homeDir, ".pet-content-creator")
os.MkdirAll(configDir, 0700)
return &ConfigService{
configPath: filepath.Join(configDir, "config.json"),
}
}
逐行解析
| 代码 | 说明 |
|---|---|
os.UserHomeDir() |
获取当前用户的 home 目录 |
filepath.Join(...) |
跨平台路径拼接(Windows 用 \,Unix 用 /) |
os.MkdirAll(..., 0700) |
递归创建目录,权限为仅所有者可读写执行 |
configPath (小写) |
小写字段 = 包外不可见,前端不会暴露这个字段 |
📌 关键概念: Wails3 只暴露 Service 的公开方法(大写开头的方法)给前端。字段(无论大小写)都不会直接暴露。这意味着
configPath字段对前端是完全隐藏的。
2.4 JSON 配置读写
Load(加载配置)
// Load 加载配置(供后端内部调用)
func (cs *ConfigService) Load() (*Config, error) {
data, err := os.ReadFile(cs.configPath)
if err != nil {
if os.IsNotExist(err) {
// 文件不存在时返回默认配置
return &Config{
DeepSeekModel: "deepseek-chat",
}, nil
}
return nil, fmt.Errorf("读取配置文件失败: %w", err)
}
var cfg Config
if err := json.Unmarshal(data, &cfg); err != nil {
return nil, fmt.Errorf("解析配置文件失败: %w", err)
}
if cfg.DeepSeekModel == "" {
cfg.DeepSeekModel = "deepseek-chat"
}
return &cfg, nil
}
💡 知识点:
%w格式化动词用于包装错误,保留原始错误链。调用方可以用errors.Is()/errors.Unwrap()检查原始错误类型。
Save(保存配置)
// Save 保存配置(供后端内部调用)
func (cs *ConfigService) Save(cfg *Config) error {
data, err := json.MarshalIndent(cfg, "", " ")
if err != nil {
return fmt.Errorf("序列化配置失败: %w", err)
}
if err := os.WriteFile(cs.configPath, data, 0600); err != nil {
return fmt.Errorf("写入配置文件失败: %w", err)
}
return nil
}
💡 知识点: 文件权限
0600= 仅所有者可读写。对于存储 API Key 的配置文件,这是基本安全习惯。
2.5 API Key 脱敏
配置保存在磁盘上是完整明文,但返回给前端时必须脱敏。否则任何人打开浏览器控制台就能看到你的 API Key。
脱敏策略
sk-abcdefghijklmnopqrstuvwxyz123456
→ sk-ab**********************3456
只保留前 4 位和后 4 位,中间替换为 ****:
// GetConfig 获取配置(前端调用)
func (cs *ConfigService) GetConfig() (*Config, error) {
cfg, err := cs.Load()
if err != nil {
return nil, err
}
// 返回脱敏后的 API Key
if len(cfg.DeepSeekAPIKey) > 8 {
cfg.DeepSeekAPIKey = cfg.DeepSeekAPIKey[:4] + "****" +
cfg.DeepSeekAPIKey[len(cfg.DeepSeekAPIKey)-4:]
}
if len(cfg.PexelsAPIKey) > 8 {
cfg.PexelsAPIKey = cfg.PexelsAPIKey[:4] + "****" +
cfg.PexelsAPIKey[len(cfg.PexelsAPIKey)-4:]
}
return cfg, nil
}
保存时处理脱敏值
用户在前端修改配置时,脱敏字段可能还是 sk-ab****3456。我们需要判断:如果传入的是脱敏值,保留磁盘上的原有 API Key:
// SaveConfig 保存配置(前端调用,处理脱敏值)
func (cs *ConfigService) SaveConfig(cfg *Config) error {
// 如果传入的是脱敏值,保留原有的 API Key
if len(cfg.DeepSeekAPIKey) > 4 && cfg.DeepSeekAPIKey[4:8] == "****" {
existing, err := cs.Load()
if err == nil && existing != nil {
cfg.DeepSeekAPIKey = existing.DeepSeekAPIKey
}
}
if len(cfg.PexelsAPIKey) > 4 && cfg.PexelsAPIKey[4:8] == "****" {
existing, err := cs.Load()
if err == nil && existing != nil {
cfg.PexelsAPIKey = existing.PexelsAPIKey
}
}
return cs.Save(cfg)
}
⚠️ 安全提示: 这种脱敏策略适合桌面应用(单用户本地运行)。如果是 Web 服务,还需要额外考虑传输层加密(HTTPS)和内存安全。
2.6 服务注册到 Wails
回到 main.go,将 ConfigService 注册为 Wails Service:
import (
"pet-content-creator/backend"
)
func main() {
// 创建配置服务(共享)
configService := backend.NewConfigService()
// 创建 Wails 应用
app := application.New(application.Options{
Name: "pet-content-creator",
Description: "AI驱动的今日头条宠物内容创作工具",
Services: []application.Service{
application.NewService(configService),
},
Assets: application.AssetOptions{
Handler: application.AssetFileServerFS(assets),
},
Mac: application.MacOptions{
ApplicationShouldTerminateAfterLastWindowClosed: true,
},
})
// ... 其余代码不变
}
编译验证
wails3 dev
此时 Wails3 会自动:
- 扫描
ConfigService的公开方法 - 在
frontend/bindings/下生成 TypeScript 绑定文件 - 前端可以
import { ConfigService } from "../bindings/..."直接调用
生成的文件大概是这样的(不需要手写):
// frontend/bindings/pet-content-creator/backend/configservice.ts
export class ConfigService {
static GetConfig(): Promise<Config | null>;
static SaveConfig(cfg: Config): Promise<void>;
}
2.7 前端调用配置服务
修改 frontend/src/App.tsx,添加配置面板:
import { useState, useEffect } from "react";
import { ConfigService } from "../bindings/pet-content-creator/backend";
interface Config {
deepseek_api_key: string;
deepseek_model: string;
pexels_api_key: string;
}
function App() {
const [showConfig, setShowConfig] = useState(false);
const [config, setConfig] = useState<Config>({
deepseek_api_key: "",
deepseek_model: "deepseek-chat",
pexels_api_key: "",
});
// 页面加载时读取配置
useEffect(() => {
ConfigService.GetConfig()
.then((cfg) => {
if (cfg) setConfig(cfg);
})
.catch(() => {});
}, []);
// 保存配置
const saveConfig = async () => {
try {
await ConfigService.SaveConfig(config);
alert("配置已保存");
setShowConfig(false);
} catch (e: any) {
alert("保存失败: " + e.message);
}
};
return (
<div className="app">
<header>
<h1>🐾 今日头条宠物内容创作工具</h1>
<button onClick={() => setShowConfig(!showConfig)}>
⚙️ 配置
</button>
</header>
{showConfig && (
<div className="config-panel">
<h3>⚙️ API 配置</h3>
<label>DeepSeek API Key</label>
<input
type="password"
value={config.deepseek_api_key}
onChange={(e) => setConfig({ ...config, deepseek_api_key: e.target.value })}
placeholder="sk-..."
/>
<label>DeepSeek 模型</label>
<input
type="text"
value={config.deepseek_model}
onChange={(e) => setConfig({ ...config, deepseek_model: e.target.value })}
/>
<label>Pexels API Key</label>
<input
type="password"
value={config.pexels_api_key}
onChange={(e) => setConfig({ ...config, pexels_api_key: e.target.value })}
placeholder="Pexels API Key"
/>
<button onClick={saveConfig}>💾 保存配置</button>
</div>
)}
</div>
);
}
export default App;
⚠️ 注意:
input type="password"会让浏览器将输入框显示为密码模式(圆点遮挡),但这只是视觉上的。实际传给后端的值仍然是明文。
2.8 服务架构模式
此时我们的后端架构如下:
┌─────────────────────────────────────┐
│ main.go (入口) │
│ │
│ configService = NewConfigService() │
│ └── 注册为 Wails Service ──→ 前端可调用
│ │
│ 其他 Service 也共享 configService │
│ (后续章节中创建) │
└─────────────────────────────────────┘
📌 关键概念:
ConfigService是一个共享服务。它在main.go中创建一次,然后传递给所有其他服务(AgentService、PexelsService 等)。这是典型的依赖注入模式。
本章总结
| 你已经学会 | 对应能力 |
|---|---|
| Go struct + JSON tag | 定义数据结构 |
os.ReadFile / os.WriteFile |
文件持久化 |
json.MarshalIndent / json.Unmarshal |
JSON 序列化 |
| API Key 脱敏策略 | 安全最佳实践 |
application.NewService(...) |
Wails3 服务注册 |
React useState + useEffect |
前端状态管理 |
🔧 动手练习
- 新增一个配置项
output_dir(输出目录路径),允许用户自定义 Word 文档保存位置 - 修改脱敏逻辑,让长度 ≤ 8 的 API Key 也能部分脱敏
- 在
config.json中手动写入错误格式的 JSON,启动应用观察错误处理是否生效 - 尝试在前端增加一个"重置配置"按钮,调用后清除所有配置项
👉 下一章:AI 文章生成 — Eino 集成 DeepSeek