這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。### 什麼是可變參數函數?可變參數函數即其參數數量是可變的 —— 0 個或多個。聲明可變參數函數的方式是在其參數類型前帶上省略符(三個點)首碼。>譯者註:“可變參數函數”在一些翻譯中也稱“變長函數”,本篇譯文中採用“可變參數函數“<p align="center">該語句聲明了一個可變參數函數及其以 “names” 命名的字串類型可變參數</p>---#### ✪ 一個簡單可變參數函數這個函數返回經過空格串連以後的參數形成的字串。```gofunc toFullname(names ...stirng) string { return strings.Join(names, " ")}```#### ✪ 你可以不傳或傳入更多的參數```gotoFullname("carl", "sagan")// output: "carl sagan"toFullname("carl")// output: "carl"toFullname()// output: ""```[線上運行代碼](https://play.golang.org/p/qqnQkBvQBP)---#### 可變參數的使用情境* 避免建立僅作傳入參數用的臨時切片* 當參數數量未知* 傳達你希望增加可讀性的意圖#### 樣本從 Go 語言標準庫中的 `fmt.Println` 函數來理解其易用性的實現。它通過可變參數函數來接收非固定數量的參數。```gofunc Prinln(a ...interface{})```如果不使用可變參數函數,其簽名將會是如下形式:```gofunc Println(params []interface{})```你便需要傳入一個切片來使用它——這確實顯得有些累贅:```gofmt.Println([]interface{}{"hello", "world"})```而它原本的使用方式是簡明愉快的:```gofmt.Println("hello", "world")fmt.Println("hello")fmt.Println()```> 之後,我們將更詳細的討論可變參數函數及示範一些常見的實際使用方式和情境### ✪ 切片和可變參數函數可變參數函數會在其內部建立一個”新的切片”。事實上,可變參數是一個簡化了切片型別參數傳入的[*文法糖*](https://en.wikipedia.org/wiki/Syntactic_sugar)。[線上運行代碼](https://play.golang.org/p/bBaWFVBsWT)---#### 不傳參數當你不傳入參數的時候,可變參數會成為一個空值切片( `nil` )。所有的非空切片都有內建的數組,而空值切片則沒有。```gofunc toFullname(names ...string) []string { return names}// names's underlying array: nil```然而,當你向空值切片添加元素時,它會自動內建一個包含該元素的數組。這個切片也就再也不是一個空值切片了。Go 語言的內建函數 “`append`” 用於向一個已有的切片追加元素,並返回更新後的切片。`append` 本身也是一個可變參數函數:```gofunc toFullname(names ...string) []string { return append(names, "hey", "what's up?")}toFullname()// output: [hey what's up?]```[線上運行代碼](https://play.golang.org/p/0RRDuGQWs_)---#### 傳入已有的切片你可以通過向一個已有的切片添加可變參數運算子 ”…“ 尾碼的方式將其傳入可變參數函數。```gonames := []string{"carl", "sagan"}toFullname(names...)// output: "carl sagan"```這就好比通常的傳參方式:```gotoFullname("carl", "sagan")```**不過,這裡還是有一點差異:**函數會在內部直接使用這個傳入的切片,並不會建立一個的新的。更多詳見下方。你也可以像下面這樣將數組轉化成切片後傳入可變參數函數:```gonames := [2]string{"carl", "sagan"}toFullname(names[:]...)```---### 一些切片傳入後的特異表現假設你傳入了一個已有的切片到某可變參數函數:```godennis := []string{"dennis", "ritchie"}toFullname(dennis...)```假設這個函數在內部改變了可變參數的第一個元素,譬如這樣:```gofunc toFullname(names ...string) string { names[0] = "guy" return strings.Join(names, " ")}```而這個修改會影響到源切片,”dennis“ 現在的值是:```go[]string{"guy", "ritchie"}```而非最初:```go[]string{"dennis", "ritchie"}```這是因為,傳入的切片和函數內部使用的切片共用同一個底層數組,因此在函數內部改變這個數組的值同樣會影響到傳入的切片:如果你直接傳入參數(不使用切片),自然就不會產生這個現象了。[線上運行代碼](https://play.golang.org/p/_-kaUnLlT0)---#### 多切片動態傳入假設我們想在傳參的同時在切片前端加上 “mr.”,然後再被函數使用。```gonames := []string{"carl", "sagan"}```於是我們先將這個切片展開,並通過 `append` 函數追加到 `[]string{"mr.")`,然後將擴充後的切片展開供 `toFullname` 可變參數函數使用:```gotoFullname(append([]string{"mr."}, names...)...)// output: "mr. carl sagan"```這與以下代碼效果相同:```gonames = append([]string{"mr."}, "carl", "sagan")toFullname(names...)// 或是這樣:toFullname([]string{"mr.", "carl", "sagan"}...)// 以及這樣——不傳入已有切片:toFullname("mr.", "carl", "sagan")```[線上運行代碼](https://play.golang.org/p/iTtz0SG_m5)---#### 返回傳入的切片傳回值的類型不可以是可變參數的形式,但你可以將它作為一個切片返回:```gofunc f(nums ...int) []int { nums[i] = 10 return nums}```當你向 `f` 函數傳入一個切片,它將返回一個新的切片。而傳入的切片和返回的切片便產生了關聯。對它們其中的的任何一方進行的所有操作都會影響到另一方(如前文所述)。```gonums := []int{23, 45, 67}nums2 := f(nums...)```這裡,`nums` 和 `nums2` 擁有相同的元素。因為它們指向同一個底層數組。```gonums = []int{10, 45, 67}nums2 = []int{10, 45, 67}```[線上運行代碼](https://play.golang.org/p/Jun14DYWvq) ? 包含對底層數組的詳細闡述---#### 擴充操作符的反例如果你的某些函數只期望接收數量可變的參數,那麼請使用可變參數函數而不是聲明一個接收切片的普通函數。```go// 反例toFullname([]string{"rob", "pike"}...)// 正例toFullname("rob", "pike")```[線上運行代碼](https://play.golang.org/p/oKQjwotLC_)---#### 使用可變參數的長度你可以通過使用可變參數的長度來調整函數的行為。```gofunc ToIP(parts ...byte) string { parts = append(parts, make([]byte, 4-len(parts))...) return fmt.Sprintf("%d.%d.%d.%d", parts[0], parts[1], parts[2], parts[3])}````ToIP` 函數接收可變參數 `parts`,然後根據 `parts` 的長度返回一個字串類型的 IP 位址,並且具有預設值 —— 0。```goToIP(255) // 255.0.0.0ToIP(10, 1) // 10.1.0.0ToIP(127, 0, 0, 1) //127.0.0.1```[線上運行代碼](https://play.golang.org/p/j9RcLvbs3K)---### ✪ 可變參數函數的函數簽名雖然可變參數函數只是一種文法糖,但由它的函數簽名——[函數類型推斷( type identity )](https://golang.org/ref/spec#Type_identity)—— 與以切片作為參數的普通函數並不相同。舉個例子,`[]string` 和 `…string` 有什麼區別呢?#### 可變參數函數的簽名:```gofunc PrintVariadic(msgs ...string)// signature: func(...string) ```#### 以切片作為參數的普通函數簽名:```gofunc PrintSlice(msgs []string)// signature: func([]string)```事實上,它們的函數類型是不同的。我們試著將它們賦值給變數來作比較:```govariadic := PrintVariadic// variadic is a func(...string)slicey := PrintSlice// slice is a func([]string)```因此,這兩者相互間並不具備可替代性```goslicey = variadic// error: type mismatch```[線上運行代碼](https://play.golang.org/p/fsZYGgTyvF)---### ✪ 混合使用可變參數及非可變參數你可以通過將非可變參數置於可變參數前面的方式來混合使用它們```gofunc toFullname(id int, names ...string) string { return fmt.Sprintf("#%02d: %s", id, strings.Join(names, " "))}toFullname(1, "carl", "sagan")// output: "#01: carl sagan"```然而,你不能在可變參數之後再聲明參數:```gofunc toFullname(id int, names ...string, age int) string {}// error```[線上運行代碼](https://play.golang.org/p/TlbDYapOCD)#### 接受多型別參數舉例來說,Go 語言標準庫中的 `Printf` 可變參數函數可以接受任何類型的參數,其實現是通過將型別宣告為一個空的介面類型( interface type )。如此你便可以使用空介面類型讓你的函數接受類型和數量都不確定的參數。```gofunc Printf(format string, a ...interface{}) (n int, err error) { /* 這是一個帶著 a... 的傳遞操作 */ return Fprintf(os.Stdout, format, a...)}fmt.Printf("%d %s %f", 1, "string", 3.14)// output: "1 string 3.14"```#### 為什麼 Printf 不只接收一個可變參數呢?當你看到 ``Printf`` 的函數簽名時,你會發現它接收一個叫 format 的字串參數和一個可變參數。```gofunc Printf(format string , a ...interface{})```**這是因為 format 是一個必要的參數**。`Printf` 強制要求提供這個參數,否則會編譯失敗。如果它將所有參數都通過一個可變參數來擷取,那麼可能導致調用者可能並沒有提供必要的 format 參數,其可讀性也不如一目瞭然的傳參方式。這種簽名清晰地告知了 `Printf` 所需要的一切。同時,當調用者沒有傳入 a 參數的時候,其函數內部會避免建立一個不必要的切片 —— 而是向我們之前看到的一樣,傳入一個空值切片( nil )。這樣可能對 `Printf` 來說並沒有太多益處,但這對你的代碼可以非常有用。你也能將這個規則實踐於你的代碼。#### 小心空介面類型`interface{}` 同時被叫做*空介面類型*,意義在於其語義本身能繞過 Go 語言的靜態類型檢查。但在不必要的情況下使用它會使你得不償失。譬如,它可能強制讓你使用[*反射*](https://blog.golang.org/laws-of-reflection),而這是一個運行時特性(而非安全且快速度的編譯時間)。你可能需要自行檢查類型錯誤,而不是讓編譯器來為你尋找他們。> *使用空介面前務必三思。基於清晰的類型或介面之上來實現你所需的函數行為會更好。*#### 通過空介面的方式向可變參數傳遞切片你不能通過空介面類型向可變參數傳遞一個普通的切片。[為什嗎?詳見此處](https://golang.org/doc/faq#convert_slice_of_interface)。```gohellos := []string{"hi", "hello", "merhaba"}```以下代碼並不能像期望的那樣跑起來:```gofmt.Printf(hellos...)```這是因為,hellos 是一個字串切片,並不是一個空介面類型。一個可變參數或者一個切片都只能從屬於某個類型。因此,你需要先將 ``hellos`` 切片轉換成空介面切片:```govar ihellos []interface = make([]interface{}, len(hellos))for i, hello := range hellos { ihellos[i] = hello}```現在這個運算式便可以工作了:```gofmt.Printf(ihellos...)// output: [hi hello merhaba]```[線上運行代碼](https://play.golang.org/p/8uRHsHFKSx)---### ✪ 對於函數式編程的實現你可以聲明一個接受數量可變的函數的可變參數函數。我們試著建立一個 formatter 函數類型。formatter 函數接受並返回一個字串:```gotype formatter func(s string) string```在聲明一個可變參數函數,接受一個字串和可變數量的 formatter 類型函數,管道式的處理這個字串,並返回處理後的結果。```gofunc format(s string, fmtrs ...formatter) string { for _, fmtr := range fmtrs { s = fmtr(s) } return s}format(" alan turing ", trim, last, strings.ToUpper)// output: TURING```[線上運行代碼](https://play.golang.org/p/kCOP6_5h-t) 線上源碼包含以上代碼的運行原理。當然,你也可以使用 channel、struct 等方式實現,而非函數式的鏈式調用規則。在[這裡](https://golang.org/pkg/io/#MultiReader)和[這裡](https://golang.org/src/text/template/parse/parse.go?s=1642:1753#L41)查看樣本。---使用切片類型的函數傳回值作為可變參數。我們重用上面的 “format func” 來建立一個可重用的格式化管道構建器:```gofunc build(f string) []formatter { switch f { case "lastUpper": return []formatter{trim, last, strings.ToUpper} case "trimUpper": return []formatter{trim, strings.ToUpper} //... default: return identityFormatter }}```然後使用擴充標識符將它的傳回值傳入 format 函數:```goformat(" alan string ", build("lastUpper")...)// output: TURING```[線上運行代碼](https://play.golang.org/p/0peZRSOVWh) 包含以上程式碼片段的詳細實現---#### 可變更配置置模式你也許在其他物件導向程式設計語言中已經熟悉此設計模式,而它於 2014 年在 Go 語言中被 [Rob Pike](https://commandcenter.blogspot.com.tr/2014/01/self-referential-functions-and-design.html) 再次推廣。它與[訪問者模式](https://en.wikipedia.org/wiki/Visitor_pattern)有些相似。該樣本也許有些超前。有任何不清楚的地方可以提問。我們建立一個 Logger,它的 verbosity 和 prefix 設定可以通過該配置模式實現在運行時被改變:```gotype Logger struct { verbosity prefix string}```SetOptions 通過可變參數為 Logger 提供一些設定來改變它的行為:```gofunc (lo *Logger) SetOptions(opts ...option) { for _, applyOptTo := range opts { applyOptTo(lo) }}```我們建立一些返回配置方法的函數,它們在一個閉包中改變 Logger 的操作行為:```gofunc HighVerbosity() option { return func(lo *Logger) { lo.verbosity = High }}func Prefix(s string) option { return func(lo *Logger) { lo.prefix = s }}```現在,我們基於預設配置聲明一個新的 Logger:```gologger := &Logger{}```然後通過上面的可變參數函數提供一些設定:```gologger.SetOptions( HighVerbosity(), Prefix("ZOMBIE CONTROL"),)```檢查輸出:```gologger.Critical("zombie outbreak!")// [ZOMBIE CONTROL] CRITICAL: zombie outbreak!logger.Info("1 second passed")// [ZOMBIE CONTROL] INFO: 1 second passed```[線上運行代碼](https://play.golang.org/p/X2XHSdYgdq) 包含以上程式碼片段的詳細實現---### ✪ 無窮無盡的精神食糧!* 在 Go 2 中,有一些改變可變參數函數表現的計劃,看[這裡](https://github.com/golang/go/issues/15209),[這裡](https://github.com/golang/go/issues/18605),還有[這裡](https://github.com/golang/go/issues/19218)。* 你可以在 Go 語言標準文檔裡找到更正式的可變參數函數指南,看[這裡](https://golang.org/ref/spec#Passing_arguments_to_..._parameters),[這裡](https://golang.org/ref/spec#Appending_and_copying_slices),[這裡](https://golang.org/ref/spec#Appending_and_copying_slices),還有[這裡](https://golang.org/ref/spec#Type_identity)。* [通過 C 語言使用可變參數函數](https://sunzenshen.github.io/tutorials/2015/05/09/cgotchas-intro.html)* 你能在[這裡](https://rosettacode.org/wiki/Variadic_function)看找到多種語言的可變參數函式宣告。盡情享用吧!我們下個教程見!
via: https://blog.learngoprogramming.com/golang-variadic-funcs-how-to-patterns-369408f19085
作者:Inanc Gumus 譯者:shockw4ver 校對:rxcai polaris1119
本文由 GCTT 原創編譯,Go語言中文網 榮譽推出
本文由 GCTT 原創翻譯,Go語言中文網 首發。也想加入譯者行列,為開源做一些自己的貢獻嗎?歡迎加入 GCTT!
翻譯工作和譯文發表僅用於學習和交流目的,翻譯工作遵照 CC-BY-NC-SA 協議規定,如果我們的工作有侵犯到您的權益,請及時聯絡我們。
歡迎遵照 CC-BY-NC-SA 協議規定 轉載,敬請在本文中標註並保留原文/譯文連結和作者/譯者等資訊。
文章僅代表作者的知識和看法,如有不同觀點,請樓下排隊吐槽
1774 次點擊