@vincent119/go-copilot-rules 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,609 @@
1
+ ---
2
+ name: go-configuration
3
+ description: |
4
+ Go 設定管理最佳實務:Viper 配置、環境變數優先級、Secrets 處理、設定驗證、
5
+ 動態重載、多環境管理、12-Factor App 原則。
6
+
7
+ **適用場景**:使用 Viper、環境變數管理、Secrets 處理、設定驗證、動態重載設定、
8
+ 多環境配置(dev/staging/prod)、Kubernetes ConfigMap/Secret 整合。
9
+
10
+ **關鍵字**:configuration, viper, environment variables, secrets, validation, env vars,
11
+ config reload, 12-factor, configmap, kubernetes config, dotenv, yaml config
12
+ ---
13
+
14
+ # Go 設定管理規範
15
+
16
+ > **相關 Skills**:本規範建議搭配 `go-observability`(日誌配置)與 `go-graceful-shutdown`(動態重載)
17
+
18
+ ---
19
+
20
+ ## 12-Factor App 原則
21
+
22
+ ### 核心原則
23
+
24
+ 1. **設定與程式碼分離**:不將設定硬編碼
25
+ 2. **環境變數優先**:使用環境變數覆蓋預設設定
26
+ 3. **Secrets 管理**:敏感資訊不進入版本控制
27
+ 4. **多環境支援**:dev/staging/prod 使用相同程式碼
28
+
29
+ ### 優先級(從高到低)
30
+
31
+ ```
32
+ 環境變數 > 設定檔 > 預設值
33
+ ```
34
+
35
+ ---
36
+
37
+ ## Viper 基本使用
38
+
39
+ ### 安裝
40
+
41
+ ```bash
42
+ go get github.com/spf13/viper
43
+ ```
44
+
45
+ ### 基本配置
46
+
47
+ ```go
48
+ package config
49
+
50
+ import (
51
+ "fmt"
52
+ "github.com/spf13/viper"
53
+ )
54
+
55
+ type Config struct {
56
+ Server ServerConfig `mapstructure:"server"`
57
+ Database DatabaseConfig `mapstructure:"database"`
58
+ Redis RedisConfig `mapstructure:"redis"`
59
+ Log LogConfig `mapstructure:"log"`
60
+ }
61
+
62
+ type ServerConfig struct {
63
+ Host string `mapstructure:"host"`
64
+ Port int `mapstructure:"port"`
65
+ }
66
+
67
+ type DatabaseConfig struct {
68
+ Host string `mapstructure:"host"`
69
+ Port int `mapstructure:"port"`
70
+ User string `mapstructure:"user"`
71
+ Password string `mapstructure:"password"` // 敏感資訊
72
+ Database string `mapstructure:"database"`
73
+ }
74
+
75
+ type RedisConfig struct {
76
+ Host string `mapstructure:"host"`
77
+ Port int `mapstructure:"port"`
78
+ Password string `mapstructure:"password"`
79
+ }
80
+
81
+ type LogConfig struct {
82
+ Level string `mapstructure:"level"`
83
+ Format string `mapstructure:"format"` // json or console
84
+ }
85
+
86
+ func Load() (*Config, error) {
87
+ // 1. 設定檔路徑
88
+ viper.SetConfigName("config") // config.yaml
89
+ viper.SetConfigType("yaml")
90
+ viper.AddConfigPath(".") // 當前目錄
91
+ viper.AddConfigPath("./configs") // ./configs 目錄
92
+ viper.AddConfigPath("/etc/myapp") // 系統目錄
93
+
94
+ // 2. 預設值
95
+ setDefaults()
96
+
97
+ // 3. 讀取設定檔
98
+ if err := viper.ReadInConfig(); err != nil {
99
+ // 設定檔不存在不算錯誤(使用預設值 + 環境變數)
100
+ if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
101
+ return nil, fmt.Errorf("read config: %w", err)
102
+ }
103
+ }
104
+
105
+ // 4. 環境變數覆蓋
106
+ viper.AutomaticEnv()
107
+ viper.SetEnvPrefix("MYAPP") // 環境變數前綴:MYAPP_SERVER_PORT
108
+
109
+ // 5. 綁定到 Struct
110
+ var cfg Config
111
+ if err := viper.Unmarshal(&cfg); err != nil {
112
+ return nil, fmt.Errorf("unmarshal config: %w", err)
113
+ }
114
+
115
+ // 6. 驗證設定
116
+ if err := cfg.Validate(); err != nil {
117
+ return nil, fmt.Errorf("validate config: %w", err)
118
+ }
119
+
120
+ return &cfg, nil
121
+ }
122
+
123
+ func setDefaults() {
124
+ viper.SetDefault("server.host", "0.0.0.0")
125
+ viper.SetDefault("server.port", 8080)
126
+ viper.SetDefault("log.level", "info")
127
+ viper.SetDefault("log.format", "json")
128
+ }
129
+ ```
130
+
131
+ ### 設定檔範例(config.yaml)
132
+
133
+ ```yaml
134
+ server:
135
+ host: 0.0.0.0
136
+ port: 8080
137
+
138
+ database:
139
+ host: localhost
140
+ port: 5432
141
+ user: postgres
142
+ password: ${DB_PASSWORD} # 從環境變數讀取(Viper 不支援)
143
+ database: myapp
144
+
145
+ redis:
146
+ host: localhost
147
+ port: 6379
148
+ password: ""
149
+
150
+ log:
151
+ level: info
152
+ format: json
153
+ ```
154
+
155
+ ---
156
+
157
+ ## 環境變數管理
158
+
159
+ ### 命名慣例
160
+
161
+ ```bash
162
+ # 格式:<PREFIX>_<SECTION>_<KEY>
163
+ export MYAPP_SERVER_PORT=8080
164
+ export MYAPP_DATABASE_HOST=postgres.example.com
165
+ export MYAPP_DATABASE_PASSWORD=secret123
166
+ export MYAPP_LOG_LEVEL=debug
167
+ ```
168
+
169
+ ### Viper 環境變數綁定
170
+
171
+ ```go
172
+ import "strings"
173
+
174
+ func Load() (*Config, error) {
175
+ // 自動替換 . 為 _
176
+ viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
177
+ viper.AutomaticEnv()
178
+ viper.SetEnvPrefix("MYAPP")
179
+
180
+ // 現在可以這樣使用:
181
+ // MYAPP_SERVER_PORT=9000 -> viper.GetInt("server.port")
182
+
183
+ var cfg Config
184
+ if err := viper.Unmarshal(&cfg); err != nil {
185
+ return nil, err
186
+ }
187
+
188
+ return &cfg, nil
189
+ }
190
+ ```
191
+
192
+ ### .env 檔案(開發環境)
193
+
194
+ ```bash
195
+ # .env
196
+ MYAPP_SERVER_PORT=8080
197
+ MYAPP_DATABASE_HOST=localhost
198
+ MYAPP_DATABASE_PASSWORD=dev_password
199
+ MYAPP_LOG_LEVEL=debug
200
+ ```
201
+
202
+ ```go
203
+ import "github.com/joho/godotenv"
204
+
205
+ func init() {
206
+ // 開發環境載入 .env(生產環境不需要)
207
+ if os.Getenv("ENV") != "production" {
208
+ _ = godotenv.Load()
209
+ }
210
+ }
211
+ ```
212
+
213
+ ---
214
+
215
+ ## Secrets 處理
216
+
217
+ ### 原則
218
+
219
+ - **不要將 Secrets 提交到版本控制**
220
+ - **使用環境變數或 Secret Management 工具**
221
+ - **限制 Secrets 存取權限**
222
+
223
+ ### 方法 1:環境變數
224
+
225
+ ```go
226
+ type DatabaseConfig struct {
227
+ Host string `mapstructure:"host"`
228
+ Port int `mapstructure:"port"`
229
+ User string `mapstructure:"user"`
230
+ Password string `mapstructure:"password"` // 從環境變數讀取
231
+ }
232
+
233
+ func main() {
234
+ // 直接從環境變數讀取
235
+ dbPassword := os.Getenv("DB_PASSWORD")
236
+ if dbPassword == "" {
237
+ log.Fatal("DB_PASSWORD not set")
238
+ }
239
+ }
240
+ ```
241
+
242
+ ### 方法 2:Kubernetes Secret
243
+
244
+ ```yaml
245
+ # k8s-secret.yaml
246
+ apiVersion: v1
247
+ kind: Secret
248
+ metadata:
249
+ name: myapp-secrets
250
+ type: Opaque
251
+ data:
252
+ db-password: cGFzc3dvcmQxMjM= # base64 編碼
253
+ ```
254
+
255
+ ```yaml
256
+ # deployment.yaml
257
+ env:
258
+ - name: MYAPP_DATABASE_PASSWORD
259
+ valueFrom:
260
+ secretKeyRef:
261
+ name: myapp-secrets
262
+ key: db-password
263
+ ```
264
+
265
+ ### 方法 3:AWS Secrets Manager / Vault
266
+
267
+ ```go
268
+ import (
269
+ "github.com/aws/aws-sdk-go/aws/session"
270
+ "github.com/aws/aws-sdk-go/service/secretsmanager"
271
+ )
272
+
273
+ func LoadSecrets() (string, error) {
274
+ sess := session.Must(session.NewSession())
275
+ svc := secretsmanager.New(sess)
276
+
277
+ input := &secretsmanager.GetSecretValueInput{
278
+ SecretId: aws.String("myapp/database/password"),
279
+ }
280
+
281
+ result, err := svc.GetSecretValue(input)
282
+ if err != nil {
283
+ return "", err
284
+ }
285
+
286
+ return *result.SecretString, nil
287
+ }
288
+ ```
289
+
290
+ ---
291
+
292
+ ## 設定驗證
293
+
294
+ ### 使用 validator
295
+
296
+ ```bash
297
+ go get github.com/go-playground/validator/v10
298
+ ```
299
+
300
+ ```go
301
+ import "github.com/go-playground/validator/v10"
302
+
303
+ type Config struct {
304
+ Server ServerConfig `mapstructure:"server" validate:"required"`
305
+ Database DatabaseConfig `mapstructure:"database" validate:"required"`
306
+ }
307
+
308
+ type ServerConfig struct {
309
+ Host string `mapstructure:"host" validate:"required,hostname|ip"`
310
+ Port int `mapstructure:"port" validate:"required,min=1,max=65535"`
311
+ }
312
+
313
+ type DatabaseConfig struct {
314
+ Host string `mapstructure:"host" validate:"required"`
315
+ Port int `mapstructure:"port" validate:"required,min=1,max=65535"`
316
+ User string `mapstructure:"user" validate:"required"`
317
+ Password string `mapstructure:"password" validate:"required,min=8"`
318
+ Database string `mapstructure:"database" validate:"required"`
319
+ }
320
+
321
+ func (c *Config) Validate() error {
322
+ validate := validator.New()
323
+ return validate.Struct(c)
324
+ }
325
+ ```
326
+
327
+ ### 自訂驗證邏輯
328
+
329
+ ```go
330
+ func (c *Config) Validate() error {
331
+ // 使用 validator
332
+ validate := validator.New()
333
+ if err := validate.Struct(c); err != nil {
334
+ return err
335
+ }
336
+
337
+ // 自訂邏輯
338
+ if c.Database.Host == "localhost" && os.Getenv("ENV") == "production" {
339
+ return errors.New("production should not use localhost database")
340
+ }
341
+
342
+ if c.Log.Level != "debug" && c.Log.Level != "info" && c.Log.Level != "warn" && c.Log.Level != "error" {
343
+ return fmt.Errorf("invalid log level: %s", c.Log.Level)
344
+ }
345
+
346
+ return nil
347
+ }
348
+ ```
349
+
350
+ ---
351
+
352
+ ## 動態重載設定
353
+
354
+ ### 監聽設定檔變更
355
+
356
+ ```go
357
+ import "github.com/fsnotify/fsnotify"
358
+
359
+ func WatchConfig(cfg *Config) {
360
+ viper.WatchConfig()
361
+ viper.OnConfigChange(func(e fsnotify.Event) {
362
+ log.Info("config file changed", zap.String("file", e.Name))
363
+
364
+ // 重新載入設定
365
+ var newCfg Config
366
+ if err := viper.Unmarshal(&newCfg); err != nil {
367
+ log.Error("failed to reload config", zap.Error(err))
368
+ return
369
+ }
370
+
371
+ if err := newCfg.Validate(); err != nil {
372
+ log.Error("invalid config after reload", zap.Error(err))
373
+ return
374
+ }
375
+
376
+ // 更新設定(需要 atomic 操作)
377
+ *cfg = newCfg
378
+ log.Info("config reloaded successfully")
379
+ })
380
+ }
381
+ ```
382
+
383
+ ### 使用 atomic.Value(安全更新)
384
+
385
+ ```go
386
+ import "sync/atomic"
387
+
388
+ type ConfigHolder struct {
389
+ cfg atomic.Value
390
+ }
391
+
392
+ func NewConfigHolder(cfg *Config) *ConfigHolder {
393
+ holder := &ConfigHolder{}
394
+ holder.cfg.Store(cfg)
395
+ return holder
396
+ }
397
+
398
+ func (h *ConfigHolder) Get() *Config {
399
+ return h.cfg.Load().(*Config)
400
+ }
401
+
402
+ func (h *ConfigHolder) Update(cfg *Config) {
403
+ h.cfg.Store(cfg)
404
+ }
405
+
406
+ // 使用範例
407
+ func main() {
408
+ cfg, _ := config.Load()
409
+ holder := NewConfigHolder(cfg)
410
+
411
+ // 監聽變更
412
+ go func() {
413
+ viper.WatchConfig()
414
+ viper.OnConfigChange(func(e fsnotify.Event) {
415
+ var newCfg Config
416
+ if err := viper.Unmarshal(&newCfg); err != nil {
417
+ log.Error("reload failed", zap.Error(err))
418
+ return
419
+ }
420
+ holder.Update(&newCfg)
421
+ })
422
+ }()
423
+
424
+ // 讀取設定(執行緒安全)
425
+ currentCfg := holder.Get()
426
+ }
427
+ ```
428
+
429
+ ---
430
+
431
+ ## 多環境管理
432
+
433
+ ### 方法 1:環境變數指定設定檔
434
+
435
+ ```go
436
+ func Load() (*Config, error) {
437
+ env := os.Getenv("ENV")
438
+ if env == "" {
439
+ env = "development"
440
+ }
441
+
442
+ configFile := fmt.Sprintf("config.%s.yaml", env)
443
+ viper.SetConfigName(configFile)
444
+ viper.AddConfigPath("./configs")
445
+
446
+ // config.development.yaml
447
+ // config.staging.yaml
448
+ // config.production.yaml
449
+
450
+ if err := viper.ReadInConfig(); err != nil {
451
+ return nil, err
452
+ }
453
+
454
+ // ...
455
+ }
456
+ ```
457
+
458
+ ### 方法 2:Base Config + Override
459
+
460
+ ```go
461
+ func Load() (*Config, error) {
462
+ // 1. 載入基礎設定
463
+ viper.SetConfigName("config.base")
464
+ viper.AddConfigPath("./configs")
465
+ if err := viper.ReadInConfig(); err != nil {
466
+ return nil, err
467
+ }
468
+
469
+ // 2. 載入環境特定設定(覆蓋)
470
+ env := os.Getenv("ENV")
471
+ if env != "" {
472
+ viper.SetConfigName(fmt.Sprintf("config.%s", env))
473
+ if err := viper.MergeInConfig(); err != nil {
474
+ // 環境設定可選
475
+ if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
476
+ return nil, err
477
+ }
478
+ }
479
+ }
480
+
481
+ // 3. 環境變數覆蓋
482
+ viper.AutomaticEnv()
483
+
484
+ // ...
485
+ }
486
+ ```
487
+
488
+ ---
489
+
490
+ ## Kubernetes ConfigMap 整合
491
+
492
+ ### ConfigMap 定義
493
+
494
+ ```yaml
495
+ # configmap.yaml
496
+ apiVersion: v1
497
+ kind: ConfigMap
498
+ metadata:
499
+ name: myapp-config
500
+ data:
501
+ config.yaml: |
502
+ server:
503
+ host: 0.0.0.0
504
+ port: 8080
505
+ log:
506
+ level: info
507
+ format: json
508
+ ```
509
+
510
+ ### Deployment 掛載
511
+
512
+ ```yaml
513
+ # deployment.yaml
514
+ apiVersion: apps/v1
515
+ kind: Deployment
516
+ metadata:
517
+ name: myapp
518
+ spec:
519
+ template:
520
+ spec:
521
+ containers:
522
+ - name: myapp
523
+ image: myapp:latest
524
+ volumeMounts:
525
+ - name: config
526
+ mountPath: /etc/myapp
527
+ readOnly: true
528
+ env:
529
+ - name: MYAPP_DATABASE_PASSWORD
530
+ valueFrom:
531
+ secretKeyRef:
532
+ name: myapp-secrets
533
+ key: db-password
534
+ volumes:
535
+ - name: config
536
+ configMap:
537
+ name: myapp-config
538
+ ```
539
+
540
+ ### 程式碼讀取
541
+
542
+ ```go
543
+ func Load() (*Config, error) {
544
+ viper.SetConfigName("config")
545
+ viper.SetConfigType("yaml")
546
+ viper.AddConfigPath("/etc/myapp") // Kubernetes 掛載路徑
547
+ viper.AddConfigPath(".") // 本地開發
548
+
549
+ if err := viper.ReadInConfig(); err != nil {
550
+ return nil, err
551
+ }
552
+
553
+ // 環境變數覆蓋(包含 Secret)
554
+ viper.AutomaticEnv()
555
+
556
+ var cfg Config
557
+ if err := viper.Unmarshal(&cfg); err != nil {
558
+ return nil, err
559
+ }
560
+
561
+ return &cfg, nil
562
+ }
563
+ ```
564
+
565
+ ---
566
+
567
+ ## 檢查清單
568
+
569
+ **基本設定**
570
+ - [ ] 設定與程式碼分離(不硬編碼)
571
+ - [ ] 使用 Viper 管理設定
572
+ - [ ] 設定多個搜尋路徑(當前目錄、系統目錄)
573
+ - [ ] 定義合理的預設值
574
+
575
+ **環境變數**
576
+ - [ ] 環境變數優先於設定檔
577
+ - [ ] 使用統一的前綴(例如 `MYAPP_`)
578
+ - [ ] 支援 `.env` 檔案(開發環境)
579
+ - [ ] 生產環境使用環境變數
580
+
581
+ **Secrets**
582
+ - [ ] Secrets 不提交到版本控制
583
+ - [ ] 使用環境變數或 Secret Management
584
+ - [ ] 限制 Secrets 存取權限
585
+ - [ ] 定期輪替 Secrets
586
+
587
+ **驗證**
588
+ - [ ] 使用 `validator` 驗證設定
589
+ - [ ] 啟動時驗證設定(Fail Fast)
590
+ - [ ] 提供清晰的錯誤訊息
591
+ - [ ] 驗證業務邏輯規則
592
+
593
+ **多環境**
594
+ - [ ] 支援 dev/staging/prod 環境
595
+ - [ ] 環境特定設定覆蓋基礎設定
596
+ - [ ] 環境變數覆蓋所有設定
597
+ - [ ] 文件說明各環境差異
598
+
599
+ **動態重載**
600
+ - [ ] 使用 `viper.WatchConfig` 監聽變更
601
+ - [ ] 重載後重新驗證設定
602
+ - [ ] 使用 `atomic.Value` 安全更新
603
+ - [ ] 記錄重載事件
604
+
605
+ **Kubernetes**
606
+ - [ ] 使用 ConfigMap 管理設定
607
+ - [ ] 使用 Secret 管理敏感資訊
608
+ - [ ] 正確設定 `volumeMounts` 路徑
609
+ - [ ] 考慮設定變更的部署策略