Goで可変長関数を使用する方法

前書き

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

可変長関数は、見かけよりも一般的です。 最も一般的なものは、fmtパッケージのPrintln関数です。

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

楕円のセット(...)が前に付いたパラメーターを持つfunctionは、可変個引数関数と見なされます。 省略記号は、指定されたパラメーターが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の値を使用して1つの引数のみを渡します。 次に、onetwoを渡し、最後にonetwo、およびthreeを渡します。

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

go run print.go

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

Output
one
one two
one two three

出力の最初の行は空白です。 これは、fmt.Printlnが最初に呼び出されたときに引数を渡さなかったためです。 2回目はoneの値が出力されました。 次に、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パラメーターをsliceとして受け取ります。 データ型はstringであるため、namesパラメーターは、関数本体内の文字列のスライス([]string)のように扱うことができます。 range演算子を使用してループを作成し、文字列のスライスを反復処理できます。

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

OutputHello 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 statementを使用して、値が渡されない場合、namesの長さは0になり、nobody to greetを出力します。

Outputnobody 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関数の区切り文字としてコンマ(,)を渡します。 次に、結合する値のスライスを渡します。 これが出力です:

OutputSammy,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
}

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

OutputSammy,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に変換しますが、引数として文字列のスライスを渡すことはできません。 これは、コンパイラが文字列の個別の引数を予期しているためです。

これを回避するには、楕円のセット(...)を接尾辞として付け、可変個引数関数に渡される個別の引数に変換することで、スライスをexplodeします。

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スライスを展開しました。

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

OutputSammy,Jessica,Drew,Jamie

ゼロ、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
}
OutputSammy,Jessica,Drew,Jamie
Sammy,Jessica,Drew,Jamie
Sammy,Jessica
Sammy

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

結論

このチュートリアルでは、可変引数関数がコードをきれいにする方法を説明しました。 必ずしもそれらを使用する必要はありませんが、便利な場合があります。

  • 関数に渡すためだけに一時スライスを作成していることがわかった場合。

  • 入力パラメーターの数が不明な場合、または呼び出されたときに変化する場合。

  • コードを読みやすくするため。

関数の作成と呼び出しの詳細については、How to Define and Call Functions in Goを参照してください。