Golang – 檔案與 I/O 操作
主題:環境變數與命令列參數(os、flag)
簡介
在實務開發中,程式往往需要根據執行環境或使用者提供的參數來調整行為。環境變數與命令列參數是兩個最常見的外部資訊來源,無論是設定資料庫連線、切換除錯模式,或是讓同一個可執行檔支援多種功能,都離不開它們。
Go 標準函式庫提供了 os 與 flag 兩個套件,分別負責存取環境變數與解析命令列旗標。這兩個套件不僅 API 簡潔、效能佳,還能與其他 I/O 操作(如檔案讀寫)無縫結合,讓程式在不同環境下保持高度可配置性。
本篇文章將從概念說明、實作範例、常見陷阱與最佳實踐,最後延伸到實務應用場景,帶領讀者一步步掌握 os 與 flag 的使用方式,適合剛入門的初學者,也能為中級開發者提供可直接套用的範本。
核心概念
1. 讀取與設定環境變數(os 套件)
| 功能 | 主要函式 | 說明 |
|---|---|---|
| 取得單一變數 | os.Getenv(key string) string |
若變數不存在,回傳空字串 |
| 取得全部變數 | os.Environ() []string |
以 "KEY=VALUE" 形式回傳切片 |
| 設定變數 | os.Setenv(key, value string) error |
只影響當前程式的環境 |
| 刪除變數 | os.Unsetenv(key string) error |
同上 |
小技巧:使用
os.LookupEnv可同時取得值與是否存在的布林,避免空字串與未設定混淆。
範例 1:基本的環境變數讀取
package main
import (
"fmt"
"os"
)
func main() {
// 直接取得環境變數,若不存在會回傳空字串
dbHost := os.Getenv("DB_HOST")
fmt.Println("DB_HOST:", dbHost)
// 使用 LookupEnv 同時取得是否存在的資訊
if port, ok := os.LookupEnv("APP_PORT"); ok {
fmt.Println("APP_PORT:", port)
} else {
fmt.Println("APP_PORT 未設定,使用預設值 8080")
}
}
說明:
LookupEnv的第二個回傳值ok能幫助我們判斷變數是否真的被設定,避免把空字串誤當成有效值。
範例 2:在程式內動態設定環境變數
package main
import (
"fmt"
"os"
)
func main() {
// 設定臨時環境變數,只對本程式有效
if err := os.Setenv("MODE", "debug"); err != nil {
panic(err)
}
// 立刻讀取剛設定的變數
mode := os.Getenv("MODE")
fmt.Println("執行模式:", mode) // 輸出: debug
// 移除變數
_ = os.Unsetenv("MODE")
}
實務觀點:在測試或臨時腳本中,常會用
Setenv來模擬不同環境,避免改動真實的系統設定。
2. 解析命令列旗標(flag 套件)
flag 讓我們以宣告式的方式定義旗標,支援 布林、字串、整數、浮點數 以及自訂型別。其核心流程:
- 宣告旗標變數(
flag.String、flag.Int…) - 呼叫
flag.Parse(),讓套件解析os.Args。 - 使用解析後的變數,或直接存取
flag.Args()取得非旗標參數。
範例 3:最簡單的旗標程式
package main
import (
"flag"
"fmt"
)
func main() {
// 定義三個旗標
name := flag.String("name", "guest", "使用者名稱")
age := flag.Int("age", 0, "使用者年齡")
verbose := flag.Bool("v", false, "啟用詳細模式")
// 解析旗標
flag.Parse()
// 取得非旗標參數(例如檔案路徑)
others := flag.Args()
fmt.Printf("Hello %s, age %d\n", *name, *age)
if *verbose {
fmt.Println("Verbose mode enabled")
}
fmt.Println("其他參數:", others)
}
執行範例
$ go run main.go -name=Alice -age=30 -v config.yaml Hello Alice, age 30 Verbose mode enabled 其他參數: [config.yaml]
範例 4:使用自訂型別(flag.Value)
有時候需要解析複雜的旗標,例如「key=value」的清單。可以自行實作 flag.Value 介面。
package main
import (
"flag"
"fmt"
"strings"
)
// KVMap 用於儲存 key=value 組
type KVMap map[string]string
func (kv *KVMap) String() string {
// 轉成 "k1=v1,k2=v2" 形式
pairs := []string{}
for k, v := range *kv {
pairs = append(pairs, fmt.Sprintf("%s=%s", k, v))
}
return strings.Join(pairs, ",")
}
// Set 會在 flag 解析時被呼叫
func (kv *KVMap) Set(value string) error {
parts := strings.SplitN(value, "=", 2)
if len(parts) != 2 {
return fmt.Errorf("invalid format, expect key=value")
}
if *kv == nil {
*kv = make(KVMap)
}
(*kv)[parts[0]] = parts[1]
return nil
}
func main() {
var cfg KVMap
flag.Var(&cfg, "cfg", "設定項目,以 key=value 形式,可多次使用")
flag.Parse()
fmt.Println("解析結果:", cfg)
}
執行範例
$ go run main.go -cfg=host=localhost -cfg=port=5432 解析結果: map[host:localhost port:5432]
範例 5:結合 os.Getenv 與 flag 的預設值
在實務中,常希望旗標的預設值能從環境變數取得,讓部署腳本更彈性。
package main
import (
"flag"
"fmt"
"os"
)
func envOr(key, fallback string) string {
if v, ok := os.LookupEnv(key); ok {
return v
}
return fallback
}
func main() {
// 旗標的預設值先從環境變數取得
dbURL := flag.String("db", envOr("DATABASE_URL", "postgres://localhost:5432/default"), "資料庫連線字串")
flag.Parse()
fmt.Println("使用的資料庫 URL:", *dbURL)
}
說明:
envOr是一個小工具函式,先檢查環境變數是否存在,若無則回傳備用的預設值。這樣在 CI/CD 流程或容器化部署時,只要設定環境變數即可,不必改動程式碼。
常見陷阱與最佳實踐
| 陷阱 | 為什麼會發生 | 建議的做法 |
|---|---|---|
忘記呼叫 flag.Parse() |
flag 只在 Parse 後才會填入變數,未呼叫會得到預設值。 |
在 main 開頭或所有旗標宣告之後立即呼叫 flag.Parse()。 |
| 使用空字串判斷環境變數是否存在 | os.Getenv 在變數未設定時回傳空字串,與「設定為空字串」無法區分。 |
改用 os.LookupEnv,取得 (value, ok) 兩個回傳值。 |
| 旗標名稱與環境變數名稱不一致 | 造成文件或腳本維護困難。 | 盡量保持 一致的命名慣例(例如全大寫環境變數、全小寫旗標),或提供映射函式。 |
大量旗標寫在 main 中 |
使程式碼難以閱讀、測試困難。 | 把旗標宣告與解析封裝成 獨立的函式(如 ParseFlags()),甚至放在 config 套件中。 |
| 旗標值未驗證 | 直接使用可能導致 runtime error(例如端口號非數字)。 | 在 flag.Parse() 後,自行驗證或使用 flag.Value 實作自訂驗證邏輯。 |
在子程序(goroutine)中呼叫 os.Setenv |
變更會影響全域環境,可能產生競爭條件。 | 避免在並行程式中動態改變環境變數,改用傳遞參數或 context。 |
最佳實踐清單
- 統一設定來源:先從環境變數取得預設值,再交給
flag覆寫,確保 CI/CD、容器與本地開發皆可共用同一套設定邏輯。 - 使用結構體保存設定:把所有旗標與環境變數聚合成
Config結構,方便傳遞與測試。 - 提供
--help說明:flag會自動產生-h/--help,確保每個旗標都有清晰的說明文字。 - 測試旗標解析:利用
flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ContinueOnError)在單元測試中模擬不同參數。 - 敏感資訊不放在環境變數:若必須使用,務必在部署平台(如 Kubernetes Secret)做好加密與存取控制。
實際應用場景
| 場景 | 為什麼需要環境變數/旗標 | 典型實作 |
|---|---|---|
| 微服務容器化 | 容器在不同環境(dev、staging、prod)需要不同的 DB 連線、日誌等設定。 | 使用 envOr 讀取 DATABASE_URL、LOG_LEVEL,旗標僅在本地開發時提供快速切換。 |
| CLI 工具 | 使用者透過指令列提供檔案路徑、模式或輸出格式。 | `flag.String("out", "json", "輸出格式 (json |
| 測試腳本 | 測試需要在不同資料庫或 API 端點間切換。 | 在測試程式 *_test.go 中 os.Setenv("API_ENDPOINT", "http://localhost:8080"),然後呼叫主程式的 ParseConfig()。 |
| 多租戶 SaaS | 每個租戶有自己的設定檔,啟動時以旗標指定租戶 ID。 | flag.Int("tenant", 0, "租戶編號"),根據 ID 從環境變數或設定檔載入對應的憑證。 |
| 自動化部署腳本 | 部署工具需要接受多個參數(如版本號、目標環境)。 | flag.String("version", "", "要部署的版本"),若未提供則從 CI_COMMIT_TAG 環境變數取得。 |
總結
- 環境變數 (
os) 與 命令列旗標 (flag) 是 Go 程式在不同執行環境下取得外部資訊的兩大入口。 - 透過
os.LookupEnv、os.Setenv、flag.String、flag.Parse等基礎 API,我們可以快速構建 可配置、可測試、易維護 的程式。 - 在實務開發中,先以環境變數提供預設值,再讓旗標覆寫,能同時滿足容器化部署與本機開發的需求。
- 注意常見陷阱(如忘記
Parse、空字串判斷)與遵守最佳實踐(統一設定來源、結構化 Config、測試解析),即可避免大部分的錯誤與維護成本。
掌握了這些概念與技巧後,你的 Go 應用將能在 多變的執行環境 中保持彈性,同時提供 清晰的使用者介面,為後續的檔案 I/O、網路通訊等功能奠定堅實基礎。祝開發順利,玩得開心!