Menu

Go言語の可変長引数:...T引数とs...による展開

Goの可変長引数関数は、1つの型の末尾の引数をいくつでも受け取ります。...Tの構文、s...でスライスを渡す方法、同じ配列を共有してしまう落とし穴、そしてfmt.Printlnが...anyを使う仕組みを解説します。

このページのコードはエディタで実行できます - 編集してすぐに結果を確認できます。

...T引数

最後の引数の型の前に ... を付けます。すると呼び出し側は、その型の引数を0個も含めていくつでも渡せます。関数の中では、その引数はスライスです。

これは 0510 と表示します。引数がないとき、nums は長さ0のnilスライスなので、ループは単に実行されません。

普通の引数を先に置くこともできます。可変長にできるのは最後の引数だけです。

func logf(level string, format string, args ...any)

s...でスライスを渡す

すでにスライスがある場合、そのまま渡すことはできません。sum(nums)cannot use nums (variable of type []int) as int value in argument to sum で失敗します。引数の後に ... を付けて展開します。

append も可変長引数関数なので、2つのスライスをつなげるのは append(a, b...) になります。

可変長の位置には展開だけを置く必要があります。sum(1, nums...)too many arguments in call to sum で失敗します。先に append で結合したスライスを作ります。

スライスはコピーされず共有される

個別の引数で呼ぶと、Goはそのための新しいスライスを作ります。既存のスライスを s... で展開すると、関数はその同じスライスを受け取ります。要素に書き込むと、呼び出し側のデータが変わります。

出力:

[7 8 9]
[0 8 9]

引数を変更する可変長引数関数は、そのことをドキュメントに書くか、先に slices.Clone でコピーするべきです。ほとんどの可変長引数関数は引数を読むだけなので問題になることはまれですが、残余引数が常に新しい配列になる言語との本当の違いです。

...anyとfmt.Printlnの仕組み

fmt.Printlnfunc Println(a ...any) (n int, err error) と宣言されています。any(Go 1.18以降の interface{} の別名)はどんな型でも受け付けるので、文字列、数値、構造体を1回の呼び出しで渡せます。

重要なのは転送のところです。logfargs...Printf に渡しています。ドットがないと Printf[]any という1つの引数を受け取り、警告の行は [WARN] [3 8] of %!d(MISSING) workers idle と表示されます。go vetmissing ... in args forwarded to printf-like function を報告するのは、fmt.Printf(format, args) のように書式の引数がそのまま渡される場合だけです。ここでは書式を + で組み立てているのでvetは何も言わず、ドットを付けるのは自分の責任です。

[]string...any に展開することはできません。要素の型が完全に一致している必要があるので、先に変換します。

names := []string{"a", "b"}
args := make([]any, len(names))
for i, n := range names {
	args[i] = n
}
fmt.Println(args...)

少なくとも1つの引数を必須にする

可変長引数は引数0個も受け付けます。関数に少なくとも1つの引数が必要なら、最初の引数を普通の引数にします。

これで maxOf() は実行時のチェックではなくコンパイルエラーになります。組み込みの max 自体も同じ仕組みで、少なくとも1つの引数が必要です。

省略可能な引数としての可変長引数

Goには引数のデフォルト値がありません。省略可能な引数を1つ装うために、可変長引数が使われることがあります。

func connect(addr string, timeout ...time.Duration)

これでも動きますが、読みにくくなります。呼び出し側はタイムアウトを3つ渡すこともでき、どれが優先されるかをシグネチャは語らないからです。オプションが2つ以上あるなら、設定用の構造体か、functional optionsパターン(...Option、各 Option はフィールドを設定する関数)を使いましょう。このパターンも可変長ですが、どの引数にも明確な意味があります。

ルールの一覧

ルール
可変長にできるのは最後の引数だけfunc f(a string, b ...int)
中では引数はスライスb の型は []int
引数0個ならnilスライスf("x") では b == nil
... でスライスを展開するf("x", nums...)
展開すると元の配列を共有するb[i] への書き込みが nums を変える
展開と追加の値は混ぜられないf("x", 1, nums...) はコンパイルできない

よくある質問

Goの可変長引数関数とは何ですか?

func sum(nums ...int) int のように、最後の引数が ...T の形になっている関数です。型 T の引数を0個以上渡して呼べ、関数の中ではその引数は []T のスライスです。

Goで可変長引数関数にスライスを渡すには?

スライスの後に ... を付けます:sum(nums...)。スライスはコピーされずにそのまま渡されるので、関数は同じ元の配列を見ます。展開と個別の引数を混ぜることはできません。sum(1, nums...) はコンパイルできません。

Goにスプレッド演算子はありますか?

可変長引数の呼び出しの最後の引数でだけ使えます:f(s...)。配列や構造体を組み立てたり、可変長でない関数をスライスで呼んだりするための一般的な展開はありません。append(a, b...) は、同じルールを組み込みの append に当てはめたものです。

Goの関数に可変長引数を2つ持たせられますか?

持たせられません。可変長にできるのは最後の引数だけで、1つの関数に1つです。追加のリストは普通のスライスの引数として渡します。

Coddy programming languages illustration

Coddyでコードを学ぼう

始める