Golang – 函數與方法
變長參數(Variadic Functions)
簡介
在日常開發中,我們常會遇到「參數個數不固定」的需求,例如日誌記錄、字串拼接或是統計計算。Go 語言提供了 變長參數(variadic) 的語法,讓函式可以接受任意數量的同類型參數,而不必事先定義多個重載版本。
變長參數不僅讓程式碼更簡潔,也提升了 API 的彈性與可讀性。對於剛踏入 Go 世界的初學者,掌握這項特性是邁向「寫得好、用得好」的關鍵一步;對於已有開發經驗的中級工程師,則能在設計通用函式庫時發揮更大的威力。
核心概念
1. 基本語法
在函式宣告的參數列表最後加上 ...,即可將該參數標記為變長。例如:
func sum(nums ...int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}
nums會被編譯器自動轉換成[]int(切片)。- 變長參數 只能放在最後一個,且每個變長函式只能有 一個 變長參數。
2. 呼叫方式
fmt.Println(sum(1, 2, 3, 4)) // 直接列出參數
fmt.Println(sum()) // 空參數,返回 0
arr := []int{5, 6, 7}
fmt.Println(sum(arr...)) // 使用已有切片,必須加上 ...
- 若已有切片想傳入變長參數,必須在切片名稱後加上
...,告訴編譯器「把切片展開」成多個參數。
3. 變長參數與普通參數混用
func greet(prefix string, names ...string) {
for _, n := range names {
fmt.Printf("%s %s\n", prefix, n)
}
}
prefix為普通參數,必須先提供;之後才是任意數量的names。
4. 多個變長參數(不允許)
Go 不允許 同時宣告兩個變長參數,例如 func foo(a ...int, b ...string) 會編譯錯誤。若需要同時接受多種型別的多參數,可考慮使用 interface{} 或自訂結構切片。
5. 變長參數的底層是切片
在函式內部,變長參數實際上是一個 只讀的切片。因此可以直接使用切片的所有操作(len, cap, append 等),但要注意 不要在外部直接修改傳入的切片,除非你已經明確了解其共享的行為。
程式碼範例
以下示範 5 個實用範例,從簡單累加到較進階的日誌與錯誤收集。
範例 1:簡易加總
func sum(nums ...int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}
// 使用
fmt.Println("Sum:", sum(1, 2, 3, 4, 5)) // Output: Sum: 15
說明:
nums內部是[]int,迭代累加後回傳結果。
範例 2:字串拼接(類似 Python 的 join)
func join(sep string, parts ...string) string {
if len(parts) == 0 {
return ""
}
result := parts[0]
for _, p := range parts[1:] {
result += sep + p
}
return result
}
// 使用
fmt.Println(join("-", "2023", "12", "19")) // Output: 2023-12-19
說明:第一個參數是分隔符,後面的
parts為任意字串。若未提供任何字串,直接回傳空字串。
範例 3:日誌函式(支援任意參數)
func Log(level string, v ...interface{}) {
// 取得時間戳記
ts := time.Now().Format("2006-01-02 15:04:05")
// 使用 fmt.Sprint 把任意型別組合成單一字串
msg := fmt.Sprint(v...)
fmt.Printf("%s [%s] %s\n", ts, level, msg)
}
// 使用
Log("INFO", "伺服器啟動於 port", 8080)
Log("ERROR", "無法連線至資料庫:", err)
說明:
...interface{}讓 Log 可以接受任何型別的參數,類似fmt.Println的彈性。
範例 4:統計資料(平均值、最大值)
func stats(nums ...float64) (avg, max float64) {
if len(nums) == 0 {
return 0, 0
}
sum := 0.0
max = nums[0]
for _, n := range nums {
sum += n
if n > max {
max = n
}
}
avg = sum / float64(len(nums))
return
}
// 使用
average, maximum := stats(3.5, 7.2, 4.8, 9.1)
fmt.Printf("Avg=%.2f Max=%.2f\n", average, maximum)
// Output: Avg=6.15 Max=9.10
說明:返回多個值時,直接使用命名返回值
avg, max,讓呼叫端更易讀。
範例 5:將切片展開傳入變長函式
func multiply(factor int, nums ...int) []int {
result := make([]int, len(nums))
for i, n := range nums {
result[i] = n * factor
}
return result
}
// 已有切片
values := []int{2, 4, 6}
scaled := multiply(3, values...) // 必須加 ...
fmt.Println(scaled) // Output: [6 12 18]
說明:
values...把切片展開成多個參數,等同於multiply(3, 2, 4, 6)。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方案 |
|---|---|---|
忘記在切片後加 ... |
直接傳入切片會被視為單一參數,編譯錯誤。 | 必須使用 slice... 形式。 |
| 變長參數不是「可變長度」的切片 | 變長參數在函式內部是 只讀 的切片,若在函式內 append,不會影響呼叫端的原始切片。 |
若需要回傳新切片,直接 return 結果;若要修改原始切片,請傳入 []T 而非變長參數。 |
| 混用多個變長參數 | Go 語法限制只能有一個變長參數。 | 使用 interface{} 或自訂結構體封裝多種型別。 |
過度使用 ...interface{} |
會失去型別安全,編譯器無法檢查錯誤。 | 只在需要高度彈性的 API(如日誌、測試框架)中使用,其他情況盡量使用具體型別。 |
| 效能考量 | 每次呼叫變長函式都會產生一個切片,若在熱點迴圈中大量呼叫,可能產生 GC 壓力。 | 可先建立切片,再使用 slice... 呼叫;或在函式內部使用 append 前先 make 足夠容量的切片。 |
最佳實踐
- 保持單一型別:盡量讓變長參數的型別具體化(如
...int、...string),避免使用...interface{}除非必要。 - 先檢查長度:在函式內部若對參數長度有特殊需求(如至少一個參數),務必先
if len(params) == 0 { … }防止 panic。 - 文件化:在函式註解中說明變長參數的預期用途與限制,讓使用者快速了解。
- 避免在變長參數上直接
append:若需要動態擴充,建議在函式外部先建立切片,再傳入。
實際應用場景
日誌與除錯
Log(level string, v ...interface{})能一次接受多個資訊,讓開發者不必自行拼字串。
資料統計與分析
- 統計函式(平均值、標準差、最大最小)常需要接受不定筆數的數值。
字串處理
- 拼接 URL、產生 CSV 行、組合錯誤訊息等,都可以透過
join(sep string, parts ...string)簡化。
- 拼接 URL、產生 CSV 行、組合錯誤訊息等,都可以透過
測試框架
assert.Equal(t, expected, actual, msg ...string)允許測試者自行添加說明訊息。
API 客戶端
- 建構 HTTP 請求時,
AddHeaders(headers ...Header)可一次加入多個自訂標頭。
- 建構 HTTP 請求時,
總結
變長參數是 Go 語言中一個 簡潔且功能強大 的特性,讓函式能彈性接受任意數量的同型別參數。掌握其語法、呼叫方式以及底層切片的行為,能讓你在撰寫日誌、字串處理、統計計算等常見需求時,寫出更乾淨、可讀性更高的程式碼。
同時,了解常見的陷阱(如忘記 ...、過度使用 interface{})以及遵守最佳實踐,能避免效能與可維護性問題。將變長參數納入你的工具箱,未來在設計公共函式庫或 API 時,就能更自信地提供彈性且安全的介面。
祝你在 Golang 的旅程中,玩得開心、寫得順手! 🚀