序章

可変個引数関数は、単一の引数として0、1、またはそれ以上の値を受け入れる関数です。 可変個引数関数は一般的なケースではありませんが、コードをよりクリーンで読みやすくするために使用できます。

可変個引数関数は、見た目よりも一般的です。 最も一般的なものは、fmtパッケージのPrintln関数です。

func Println(a ...interface{}) (n int, err error)

省略記号のセット(...)が前に付いたパラメーターを持つ関数は、可変個引数関数と見なされます。 省略記号は、提供されるパラメーターが0、1、またはそれ以上の値であることを意味します。 fmt.Printlnパッケージの場合、パラメーターaが可変個引数であると記載されています。

fmt.Println関数を使用して、0、1、またはそれ以上の値を渡すプログラムを作成してみましょう。

print.go
package main

import "fmt"

func main() {
	fmt.Println()
	fmt.Println("one")
	fmt.Println("one", "two")
	fmt.Println("one", "two", "three")
}

fmt.Printlnを初めて呼び出すときは、引数を渡しません。 2回目にfmt.Printlnを呼び出すときは、oneの値を持つ単一の引数のみを渡します。 次に、onetwoを渡し、最後にonetwothreeを渡します。

次のコマンドでプログラムを実行してみましょう。

  1. go run print.go

次の出力が表示されます。

Output
one one two one two three

出力の最初の行は空白です。 これは、fmt.Printlnが最初に呼び出されたときに引数を渡さなかったためです。 oneの値が2回目に印刷されました。 次にonetwo、最後にonetwothreeです。

可変個引数関数を呼び出す方法を確認したので、独自の可変個引数関数を定義する方法を見てみましょう。

可変個引数関数の定義

引数の前に省略記号(...)を使用すると、可変個引数関数を定義できます。 名前が関数に送信されたときに人々に挨拶するプログラムを作成しましょう。

hello.go
package main

import "fmt"

func main() {
	sayHello()
	sayHello("Sammy")
	sayHello("Sammy", "Jessica", "Drew", "Jamie")
}

func sayHello(names ...string) {
	for _, n := range names {
		fmt.Printf("Hello %s\n", n)
	}
}

namesという単一のパラメーターのみを受け取るsayHello関数を作成しました。 データ型...stringの前に省略記号(...)を付けるため、パラメーターは可変個引数です。 これは、関数が0、1、または多くの引数を受け入れることができることをGoに通知します。

sayHello関数は、namesパラメーターをスライスとして受け取ります。 データ型はstringであるため、namesパラメーターは、関数本体内の文字列のスライス([]string)のように扱うことができます。 range 演算子を使用してループを作成し、文字列のスライスを反復処理できます。

プログラムを実行すると、次の出力が得られます。

Output
Hello Sammy Hello Sammy Hello Jessica Hello Drew Hello Jamie

sayHelloを初めて呼び出したときに何も印刷されなかったことに注意してください。 これは、可変個引数パラメーターがstringの空のsliceであったためです。 スライスをループしているため、反復処理するものはなく、fmt.Printfが呼び出されることはありません。

値が送信されなかったことを検出するようにプログラムを変更してみましょう。

hello.go
package main

import "fmt"

func main() {
	sayHello()
	sayHello("Sammy")
	sayHello("Sammy", "Jessica", "Drew", "Jamie")
}

func sayHello(names ...string) {
	if len(names) == 0 {
		fmt.Println("nobody to greet")
		return
	}
	for _, n := range names {
		fmt.Printf("Hello %s\n", n)
	}
}

ここで、 ifステートメントを使用して、値が渡されない場合、namesの長さは0になり、nobody to greetを出力します。

Output
nobody to greet Hello Sammy Hello Sammy Hello Jessica Hello Drew Hello Jamie

可変個引数パラメーターを使用すると、コードが読みやすくなります。 指定された区切り文字で単語を結合する関数を作成しましょう。 最初に可変個引数関数を使用せずにこのプログラムを作成して、次のように読み取る方法を示します。

join.go
package main

import "fmt"

func main() {
	var line string

	line = join(",", []string{"Sammy", "Jessica", "Drew", "Jamie"})
	fmt.Println(line)

	line = join(",", []string{"Sammy", "Jessica"})
	fmt.Println(line)

	line = join(",", []string{"Sammy"})
	fmt.Println(line)
}

func join(del string, values []string) string {
	var line string
	for i, v := range values {
		line = line + v
		if i != len(values)-1 {
			line = line + del
		}
	}
	return line
}

このプログラムでは、join関数の区切り文字としてコンマ(,)を渡します。 次に、結合する値のスライスを渡します。 出力は次のとおりです。

Output
Sammy,Jessica,Drew,Jamie Sammy,Jessica Sammy

この関数はvaluesパラメーターとして文字列のスライスを受け取るため、join関数を呼び出すときに、すべての単語をスライスでラップする必要がありました。 これにより、コードが読みにくくなる可能性があります。

ここで、同じ関数を記述しましょう。ただし、可変個引数関数を使用します。

join.go
package main

import "fmt"

func main() {
	var line string

	line = join(",", "Sammy", "Jessica", "Drew", "Jamie")
	fmt.Println(line)

	line = join(",", "Sammy", "Jessica")
	fmt.Println(line)

	line = join(",", "Sammy")
	fmt.Println(line)
}

func join(del string, values ...string) string {
	var line string
	for i, v := range values {
		line = line + v
		if i != len(values)-1 {
			line = line + del
		}
	}
	return line
}

プログラムを実行すると、前のプログラムと同じ出力が得られることがわかります。

Output
Sammy,Jessica,Drew,Jamie Sammy,Jessica Sammy

join関数の両方のバージョンはプログラムでまったく同じことを行いますが、関数の可変個引数バージョンは、呼び出されたときにはるかに読みやすくなります。

可変引数引数の順序

関数には可変個引数パラメーターを1つだけ含めることができ、それは関数で定義された最後のパラメーターである必要があります。 可変個引数関数で最後のパラメーター以外の順序でパラメーターを定義すると、コンパイルエラーが発生します。

join.go
package main

import "fmt"

func main() {
	var line string

	line = join(",", "Sammy", "Jessica", "Drew", "Jamie")
	fmt.Println(line)

	line = join(",", "Sammy", "Jessica")
	fmt.Println(line)

	line = join(",", "Sammy")
	fmt.Println(line)
}

func join(values ...string, del string) string {
	var line string
	for i, v := range values {
		line = line + v
		if i != len(values)-1 {
			line = line + del
		}
	}
	return line
}

今回は、join関数の最初にvaluesパラメーターを配置します。 これにより、次のコンパイルエラーが発生します。

Output
./join_error.go:18:11: syntax error: cannot use ... with non-final parameter values

可変個引数関数を定義する場合、最後のパラメーターのみを可変個引数にすることができます。

爆発的な議論

これまで、可変個引数関数に0、1、またはそれ以上の値を渡すことができることを確認しました。 ただし、値のスライスがあり、それらを可変個引数関数に送信したい場合があります。

前のセクションのjoin関数を見て、何が起こるかを見てみましょう。

join.go
package main

import "fmt"

func main() {
	var line string

	names := []string{"Sammy", "Jessica", "Drew", "Jamie"}

	line = join(",", names)
	fmt.Println(line)
}

func join(del string, values ...string) string {
	var line string
	for i, v := range values {
		line = line + v
		if i != len(values)-1 {
			line = line + del
		}
	}
	return line
}

このプログラムを実行すると、コンパイルエラーが発生します。

Output
./join-error.go:10:14: cannot use names (type []string) as type string in argument to join

可変個引数関数はvalues ...stringのパラメーターを文字列のスライス[]stringに変換しますが、引数として文字列のスライスを渡すことはできません。 これは、コンパイラが文字列の個別の引数を予期しているためです。

これを回避するには、スライスに省略記号のセット(...)を付加し、可変個引数関数に渡される個別の引数に変換することで、スライスを展開できます。

join.go
package main

import "fmt"

func main() {
	var line string

	names := []string{"Sammy", "Jessica", "Drew", "Jamie"}

	line = join(",", names...)
	fmt.Println(line)
}

func join(del string, values ...string) string {
	var line string
	for i, v := range values {
		line = line + v
		if i != len(values)-1 {
			line = line + del
		}
	}
	return line
}

今回は、join関数を呼び出すときに、省略記号(...)を追加してnamesスライスを展開しました。

これにより、プログラムを期待どおりに実行できるようになります。

Output
Sammy,Jessica,Drew,Jamie

分解するスライスだけでなく、0、1、またはそれ以上の引数を渡すことができることに注意することが重要です。 これまでに見たすべてのバリエーションを渡すコードは次のとおりです。

join.go
package main

import "fmt"

func main() {
	var line string

	line = join(",", []string{"Sammy", "Jessica", "Drew", "Jamie"}...)
	fmt.Println(line)

	line = join(",", "Sammy", "Jessica", "Drew", "Jamie")
	fmt.Println(line)

	line = join(",", "Sammy", "Jessica")
	fmt.Println(line)

	line = join(",", "Sammy")
	fmt.Println(line)

}

func join(del string, values ...string) string {
	var line string
	for i, v := range values {
		line = line + v
		if i != len(values)-1 {
			line = line + del
		}
	}
	return line
}
Output
Sammy,Jessica,Drew,Jamie Sammy,Jessica,Drew,Jamie Sammy,Jessica Sammy

これで、0、1、または多くの引数と、分解したスライスを可変個引数関数に渡す方法がわかりました。

結論

このチュートリアルでは、可変個引数関数によってコードがどのようにクリーンになるかを見てきました。 常に使用する必要はありませんが、便利な場合があります。

  • 関数に渡すためだけに一時的なスライスを作成していることがわかった場合。
  • 入力パラメータの数が不明であるか、呼び出されたときに変化する場合。
  • コードを読みやすくするため。

関数の作成と呼び出しの詳細については、Goで関数を定義して呼び出す方法を参照してください。