本文 AI 產出,尚未審核

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 讓我們以宣告式的方式定義旗標,支援 布林、字串、整數、浮點數 以及自訂型別。其核心流程:

  1. 宣告旗標變數(flag.String、flag.Int…)
  2. 呼叫 flag.Parse(),讓套件解析 os.Args。
  3. 使用解析後的變數,或直接存取 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。

最佳實踐清單

  1. 統一設定來源:先從環境變數取得預設值,再交給 flag 覆寫,確保 CI/CD、容器與本地開發皆可共用同一套設定邏輯。
  2. 使用結構體保存設定:把所有旗標與環境變數聚合成 Config 結構,方便傳遞與測試。
  3. 提供 --help 說明:flag 會自動產生 -h/--help,確保每個旗標都有清晰的說明文字。
  4. 測試旗標解析:利用 flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ContinueOnError) 在單元測試中模擬不同參數。
  5. 敏感資訊不放在環境變數:若必須使用,務必在部署平台(如 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、網路通訊等功能奠定堅實基礎。祝開發順利,玩得開心!