覚えたら書く

IT関係のデベロッパとして日々覚えたことを書き残したいです。twitter: @yyoshikaw

Go言語 - 関数

オブジェクト指向ではないGo言語において、関数を定義することがプログラミングにおいてかなり重要なものとなるようです。
ここでは、Go言語の関数について基本的な部分を確認します。
Go言語の関数はJavaのメソッドやC言語の関数と比べても独特な部分がいくつも見受けられます。


基本的な関数の定義

Go言語の関数定義は基本的に以下の構文になります

func <関数名>([引数]) [戻り値の型] {
    [関数の本体]
}


以下は2つの文字列(string)を引数に取り、それらの文字列を結合した文字列の文字数を戻り値(int)で返す関数になります

func concatCount(str1 string, str2 string) int {
    concatStr := str1 + str2
    return utf8.RuneCountInString(concatStr)
}

定義した関数を呼び出すには以下の通りとなります

count := concatCount("Hello", "World")
fmt.Println("count:", count)   // ⇒ count: 10


■引数無しの関数定義

関数定義において引数を記述しなければ引数なしの関数となります。

func number() int {
    return 10
}


■戻り値無しの関数定義

関数定義において戻り値の型を記述しなければ戻り値なしの関数となります。
JavaやC言語では必要になるvoidの記述はGo言語では不要となります

func printHelloWorld() {
    fmt.Println("Hello, World")
}


■引数の型をまとめて記述できる

同じ型の引数を複数持つ関数を定義する場合、

func concatCount(str1 string, str2 string) int {
    concatStr := str1 + str2
    return utf8.RuneCountInString(concatStr)
}

以下のように、引数の型をまとめて記述することができます。

func concatCount(str1, str2 string) int {
    concatStr := str1 + str2
    return utf8.RuneCountInString(concatStr)
}


可変個引数関数

可変個の引数を渡すことができる関数を定義することもできます。そのような場合は引数の型の前に"..."を付与します。
(ちなみに、可変個引数にできるのは、関数の最後のパラメータのみです)

例として、可変個の整数を引数にとってその合計を返す関数を定義すると以下のようになります

func sumAll1(values ...int) int {
    length := len(values)

    sum := 0
    for _, value := range values {
        sum += value
    }

    fmt.Printf("func sumAll [param: len=%d, value=%#v, result -> %d]\n", length, values, sum)

    return sum
}


呼び出し側のコードは以下のようになります。

var sumVal int
sumVal = sumAll1()         // ⇒ 0
sumVal = sumAll1(5)       // ⇒ 5
sumVal = sumAll1(5, 6, 7, 8, 9) // ⇒ 35

// sumVal = sumAll1([]int{1, 2, 3, 4}) // ← コンパイルエラー。スライスを渡すことはできない

■実行結果

func sumAll1 [param: len=0, value=[]int(nil), result -> 0]
func sumAll1 [param: len=1, value=[]int{5}, result -> 5]
func sumAll1 [param: len=5, value=[]int{5, 6, 7, 8, 9}, result -> 35]

関数呼び出し側のコードと実行結果から以下が分かります。

  • 引数は複数でなくても良い。1個でも0個でも良い
  • 関数に渡された引数の値はスライスとして扱われる。ただし、呼び出す際の引数にスライスを渡すことはできない


引数がスライスになっている関数を定義して、その関数と可変個引数関数の型を各々チェックしてみます

func sumAll2(values []int) int {
    sum := 0
    for _, value := range values {
        sum += value
    }
    return sum
}

func main() {
    fmt.Printf("sumAll1 -> %T\n", sumAll1)
    fmt.Printf("sumAll2 -> %T\n", sumAll2)
}

■実行結果

sumAll1 -> func(...int) int
sumAll2 -> func([]int) int

結果から、可変個引数関数とスライスを引数に取る関数の型が明確に違うことが分かります


複数の戻り値を返す

Go言語では戻り値を複数返すことができます。
JavaやC言語からするとかなり不思議な感じがします。

以下のような定義をします。複数の戻り値の型の前後に記述している括弧は省略できません

func <関数名>([引数]) (戻り値1の型, 戻り値2の型 [, 戻り値3の型, 戻り値4の型・・・]) {
    [関数の本体]
}


例えば、2つの文字列(string)を引数に取り、それらの文字列を結合した文字列(第1戻り値)とその文字列の文字数(第2戻り値)を返す関数の定義は以下のようになります

func concatCount(str1 string, str2 string) (string, int) {
    concatStr := str1 + str2
    count := utf8.RuneCountInString(concatStr)
    return concatStr, count
}

呼び出し方法は以下のようになり、変数をカンマ区切りで並べて関数が返す複数の戻り値を各変数に格納します

str1 := "Hello"
str2 := "World"

str, count := concatCount(str1, str2)  // str: 結合した文字列, count: 文字数

fmt.Printf("str=%s -> count: %d", str, count)   // ⇒ str=HelloWorld -> count: 10


戻り値を破棄する

関数が複数の戻り値を返す場合でも、そのすべての戻り値が関数呼び出し後の処理で必要ではないケースもあります。
そのような場合には、戻り値を破棄することができます。
具体的には戻り値用の変数の代わりに"_"を利用して戻り値を破棄します。

例えば先の例で定義したconcatCount関数において、
結合した文字列(第1戻り値)が不要という場合は以下のように呼び出します

str1 := "Hello"
str2 := "World"

_, count := concatCount(str1, str2)  // 第1戻り値を破棄
fmt.Printf("str1=%s, str2=%s -> concat count: %d", str1, str2, count)  // ⇒ str1=Hello, str2=World -> concat count: 10

上記とは逆に、結合した文字列の文字数(第2戻り値)が不要という場合は以下のように呼び出します

str1 := "Hello"
str2 := "World"


str, _ := concatCount(str1, str2)  // 第2戻り値を破棄
fmt.Printf("str1=%s, str2=%s -> concat str: %s\n", str1, str2, str)  // ⇒ str1=Hello, str2=World -> concat str: HelloWorld


関数の戻り値を利用したエラー処理

Go言語には例外の機構は基本的にはありません。Go言語のエラー処理は関数の戻りを利用して行うスタイルが基本になります。
この辺はC言語と似ていますが、Cと違うのは関数の戻り値が複数返せるということです。
複数戻り値が返せることを利用して、普通に返したい値とエラー判定用の値の両方を返して呼び出し側でエラー判定用の(error型等の)戻り値をチェックしてエラー処理を行います。

エラー判定用の戻り値は基本的に、複数戻り値の最後の戻り値として返します。
また呼び出し側では、エラー判定用のerror型の値を受ける変数の変数名にはerrというのを使うのが慣例の様です


例えば、エラーを返す可能性のある以下のような関数があった場合、

// 戻り値は、返したい値とerror
func concatCount(str1 string, str2 string) (int, error) {
    if len(str1) == 0 || len(str2) == 0 {
        return 0, errors.New("param is empty")
    }

    concatStr := str1 + str2
    count := utf8.RuneCountInString(concatStr)
    return count, nil
}

呼び出し側は以下のようになります

str1 := "Hello"
str2 := ""

count, err := concatCount(str1, str2)
if err != nil {
    fmt.Printf("Failed concatAndPrint") // 何らかのエラー処理
    return
}

fmt.Printf("str1=%s, str2=%s -> concat count: %d", str1, str2, count)

第2戻り値をerrで受けて、そのerr変数がnilかどうかでエラー判定をします(nilでなければエラー) errに対するnilチェックが基本的にGo言語におけるエラー判定処理となります。


また、戻り値がerror型のみとなっている以下のような関数があった場合

func concatAndPrint(str1 string, str2 string) error {
    if len(str1) == 0 || len(str2) == 0 {
        return errors.New("param is empty")
    }

    concatStr := str1 + str2
    fmt.Println(concatStr)
    return nil
}

呼び出し側は以下のようになり、err変数でのエラー結果の受け取りとエラー判定を一気にやるような記述をします

str1 := "Hello"
str2 := ""

if err := concatAndPrint(str1, str2); err != nil {
    fmt.Printf("Failed concatAndPrint") // 何らかのエラー処理
}


戻り値に変数を割り当てる

これもまたGo言語独特な気がしますが、関数定義において戻り値の型だけではなく戻り値の変数名も指定することができます。

たとえば以下のような関数がそれに当たり、resStrとstrCountが戻り値用の変数となります。

// 戻り値に変数を割り当てる
func concatCount(str1 string, str2 string) (retStr string, strCount int) {
    retStr = str1 + str2
    strCount = utf8.RuneCountInString(retStr)
    return
}

戻り値の変数名を指定している場合は、同名のローカル変数に格納した値が戻り値となります。
return文では何も返す必要はありません。(自動的にローカル変数の値が戻り値として返されます)


まとめ

Go言語における基本的な関数の定義や扱いについて書いてみました。
エントリ内に書いた通りですが、関数に関しては他言語であまり見かけない結構独特な仕様をGo言語は持っています。

本エントリでは触れなかった無名関数については以下エントリを参照下さい。



関連エントリ