golang 可変長引数 で検索しても本題の情報を得られなかったのでここにまとめます。
Go の可変長引数( variadic parameters )を使用する際に注意すべき重要な挙動と、安全なコーディング手法について解説します。
可変長引数の基本的な挙動
可変長引数を受け取る関数は、スライスに ...
をつけて渡すとそのスライスが渡されます。
問題のあるコード
package main
import "fmt"
// processNumbers は可変長引数で受け取った値をそれぞれ2倍してスライスで返します。
func processNumbers(numbers ...int) []int {
fmt.Printf("受け取ったスライス: %v\n", numbers)
for i := range numbers {
numbers[i] *= 2 // 各要素を2倍にする
}
fmt.Printf("返すスライス: %v\n", numbers)
return numbers
}
個別の値を渡す場合
package main
import "fmt"
func main() {
result := processNumbers(1, 2, 3) // 個別の値として渡す
// この場合、関数を呼び出すときに新しいスライスが作成される
fmt.Printf("実行結果: %v\n", result) // [2, 4, 6]
}
スライスに...
をつけて渡す場合
package main
import "fmt"
func main() {
original := []int{1, 2, 3}
fmt.Printf("実行前: %v\n", original) // [1, 2, 3]
result := processNumbers(original...) // スライスを展開して渡す
// この場合、スライスがそのまま processNumbers に渡される。
fmt.Printf("実行結果: %v\n", result) // [2, 4, 6]
fmt.Printf("元のスライス: %v\n", original) // [2, 4, 6] え?
}
Playground で確認する
サンプルコード
package main
import "fmt"
func dangerousFunction(numbers ...int) []int {
// 受け取ったスライスをそのまま変更してしまう
for i := range numbers {
numbers[i] *= 2 // 各要素を2倍にする
}
return numbers
}
func main() {
fmt.Printf("ケース1: 個別の値を渡す場合(問題なし)\n")
result1 := dangerousFunction(1, 2, 3)
fmt.Printf("結果1: %v\n", result1) // [2, 4, 6]
fmt.Printf("ケース2: スライスを...で渡す場合(問題あり)\n")
original := []int{1, 2, 3}
fmt.Printf("処理前: %v\n", original) // [1, 2, 3]
result2 := dangerousFunction(original...)
fmt.Printf("結果2: %v\n", result2) // [2, 4, 6]
fmt.Printf("元のスライス: %v\n", original) // [2, 4, 6] - 変更されてしまった!
// 呼び出し元は元のスライスが変更されることを期待していない
// これは予期しない副作用となる
}
関数内での判別は不可能
残念ながら、可変長引数を受け取る関数内で、引数が ...
を使って渡されたスライスなのか、個別の値なのかを確実に判別する方法はありません。
package main
import "fmt"
func cannotDistinguish(args ...int) {
// 個別の値を渡したときは容量は長さと同じになるらしいが、
// 確実に判別できない
if len(args) == cap(args) {
fmt.Println("...で渡された可能性が高い(ただし確実ではない)")
}
}
安全な対処法:必ずコピーを作成する
可変長引数で受け取ったスライスを変更する場合は、必ず事前にコピーを作成しましょう。
package main
import "slices"
func safeFunctionWithClone(numbers ...int) []int {
// Go 1.21以降で利用可能
result := slices.Clone(numbers)
// コピーに対して変更を行う
for i := range result {
result[i] *= 2
}
return result
}
まとめ
-
可変長引数の挙動を理解する:
...
を使ってスライスを渡すと、受け取った関数でも同じスライスを参照する。関数内で引数の渡し方を判別する確実な方法はない - 常にコピーを作成: 可変長引数で受け取ったスライスを変更する場合は、必ず事前にコピーを作成するなど、動作が明確になるように設計を工夫する
- ドキュメント化: 関数の動作(副作用の有無)を明確にドキュメント化する
これらの原則に従うことで、予期しない副作用を避け、安全で保守性の高いGoコードを書くことができます。