関数

複数の戻り値

Goの珍しい特徴の1つは、関数とメソッドが複数の値を返すことができることです。この形式は、Cプログラムでのいくつかの不器用な慣用句を改善するために使用できます:EOF-1のような帯域内エラー戻り値や、アドレスで渡される引数の変更など。

Cでは、書き込みエラーは負のカウントで通知され、エラーコードは不安定な場所に隠されます。Goでは、Writeはカウントエラーを返すことができます:「はい、何バイトか書きましたが、デバイスがいっぱいになったためすべてではありません」。パッケージosWriteメソッドのシグネチャは次のとおりです:

func (file *File) Write(b []byte) (n int, err error)

ドキュメントが述べているように、n != len(b)のときに書き込まれたバイト数と非nilerrorを返します。これは一般的なスタイルです。詳細については、エラー処理に関するセクションを参照してください。

同様のアプローチは、参照パラメータをシミュレートするために戻り値へのポインタを渡す必要性を回避します。バイト配列内の位置から数値を取得し、数値と次の位置を返すこの単純な関数を次に示します。

func nextInt(b []byte, i int) (int, int) {
    for ; i < len(b) && !isDigit(b[i]); i++ {
    }
    x := 0
    for ; i < len(b) && isDigit(b[i]); i++ {
        x = x*10 + int(b[i]) - '0'
    }
    return x, i
}

次のように使用して、入力スライスb内の数値をスキャンできます:

for i := 0; i < len(b); {
    x, i = nextInt(b, i)
    fmt.Println(x)
}

名前付き結果パラメータ

Go関数の戻り値または結果「パラメータ」には名前を付けることができ、受信パラメータと同じように通常の変数として使用できます。名前を付けると、関数が開始されるときにその型のゼロ値に初期化されます。関数が引数なしでreturnステートメントを実行すると、結果パラメータの現在の値が戻り値として使用されます。

名前は必須ではありませんが、コードを短く明確にすることができます:それらはドキュメントです。nextIntの結果に名前を付けると、どちらの返されたintがどれなのかが明らかになります。

func nextInt(b []byte, pos int) (value, nextPos int) {

名前付き結果は初期化され、装飾のない戻り値に結び付けられているため、明確にし、単純化することもできます。これは、それらをうまく使用したio.ReadFullのバージョンです:

func ReadFull(r Reader, buf []byte) (n int, err error) {
    for len(buf) > 0 && err == nil {
        var nr int
        nr, err = r.Read(buf)
        n += nr
        buf = buf[nr:]
    }
    return
}

Defer

Go のdeferステートメントは、deferを実行する関数が戻る直前に実行されるように関数呼び出し(遅延された関数)をスケジュールします。これは珍しいが効果的な方法で、関数が戻るためにどのパスを取るかに関係なく、解放されなければならないリソースなどの状況を処理します。典型的な例は、ミューテックスのロック解除やファイルのクローズです。

// Contents はファイルの内容を文字列として返します
func Contents(filename string) (string, error) {
    f, err := os.Open(filename)
    if err != nil {
        return "", err
    }
    defer f.Close()  // f.Close は終了時に実行されます

    var result []byte
    buf := make([]byte, 100)
    for {
        n, err := f.Read(buf[0:])
        result = append(result, buf[0:n]...) // append は後で説明します
        if err != nil {
            if err == io.EOF {
                break
            }
            return "", err  // ここで戻る場合、f はクローズされます
        }
    }
    return string(result), nil // ここで戻る場合、f はクローズされます
}

Closeのような関数の呼び出しを遅延させることには2つの利点があります。第1に、ファイルをクローズすることを忘れることがないことを保証し、後で関数を編集して新しい戻りパスを追加する場合に犯しがちな間違いを防ぎます。第2に、クローズが開いた場所の近くに配置されることを意味し、関数の最後に配置するよりもはるかに明確です。

遅延された関数の引数(関数がメソッドの場合はレシーバーを含む)は、deferが実行されるときに評価され、呼び出しが実行されるときではありません。関数が実行されるにつれて変数が値を変更することを心配する必要がないことに加えて、これは単一の遅延された呼び出しサイトが複数の関数実行を遅延できることを意味します。ばかげた例を次に示します。

for i := 0; i < 5; i++ {
    defer fmt.Printf("%d ", i)
}

遅延された関数は LIFO 順序で実行されるため、このコードは関数が戻るときに4 3 2 1 0を出力します。より現実的な例は、プログラムを通じて関数実行をトレースする簡単な方法です。いくつかの簡単なトレースルーチンを次のように書くことができます:

func trace(s string)   { fmt.Println("entering:", s) }
func untrace(s string) { fmt.Println("leaving:", s) }

// 次のように使用します:
func a() {
    trace("a")
    defer untrace("a")
    // 何かをする....
}

遅延された関数の引数がdefer実行時に評価されるという事実を利用することで、より良くできます。トレースルーチンは、untraceルーチンの引数を設定できます。この例:

func trace(s string) string {
    fmt.Println("entering:", s)
    return s
}

func un(s string) {
    fmt.Println("leaving:", s)
}

func a() {
    defer un(trace("a"))
    fmt.Println("in a")
}

func b() {
    defer un(trace("b"))
    fmt.Println("in b")
    a()
}

func main() {
    b()
}

は次を出力します:

entering: b
in b
entering: a
in a
leaving: a
leaving: b

他の言語からブロックレベルのリソース管理に慣れているプログラマーには、deferは特異に見えるかもしれませんが、その最も興味深く強力なアプリケーションは、ブロックベースではなく関数ベースであるという事実に正確に由来します。panicrecoverに関するセクションでは、その可能性の別の例を見ることができます。