Go言語入門:効果的なGo -関数:defer文-

スポンサーリンク
Go言語入門:効果的なGo -関数:defer文- ノウハウ
Go言語入門:効果的なGo -関数:defer文-
この記事は約9分で読めます。
よっしー
よっしー

こんにちは。よっしーです(^^)

本日は、Go言語を効果的に使うためのガイドラインについて解説しています。

スポンサーリンク

背景

Go言語を学び始めて、より良いコードを書きたいと思い、Go言語の公式ドキュメント「Effective Go」を知りました。これは、いわば「Goらしいコードの書き方指南書」になります。単に動くコードではなく、効率的で保守性の高いコードを書くためのベストプラクティスが詰まっているので、これを読んだ時の内容を備忘として残しました。

関数における「defer文」

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

// Contents returns the file's contents as a string.
func Contents(filename string) (string, error) {
    f, err := os.Open(filename)
    if err != nil {
        return "", err
    }
    defer f.Close()  // f.Close will run when we're finished.

    var result []byte
    buf := make([]byte, 100)
    for {
        n, err := f.Read(buf[0:])
        result = append(result, buf[0:n]...) // append is discussed later.
        if err != nil {
            if err == io.EOF {
                break
            }
            return "", err  // f will be closed if we return here.
        }
    }
    return string(result), nil // f will be closed if we return here.
}

Closeのような関数への呼び出しを遅延することには2つの利点があります。第一に、ファイルを閉じることを決して忘れないことが保証されます。これは、後で関数を編集して新しいreturnパスを追加する場合に犯しやすいミスです。第二に、closeがopenの近くに配置されることを意味し、これは関数の最後に配置するよりもはるかに明確です。

遅延された関数への引数(関数がメソッドの場合はレシーバーを含む)は、呼び出しが実行される時ではなく、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) }

// Use them like this:
func a() {
    trace("a")
    defer untrace("a")
    // do something....
}

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

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のセクションでは、その可能性の別の例を見るでしょう。

解説

1. deferの基本概念

defer文は関数の終了直前に指定した処理を実行する仕組みです。これにより、リソースの確実な解放やクリーンアップ処理が保証されます。

2. Contents関数の解析

func Contents(filename string) (string, error) {
    f, err := os.Open(filename)
    if err != nil {
        return "", err
    }
    defer f.Close()  // ここでファイルクローズを予約

    var result []byte
    buf := make([]byte, 100)
    for {
        n, err := f.Read(buf[0:])
        result = append(result, buf[0:n]...)
        if err != nil {
            if err == io.EOF {
                break
            }
            return "", err  // ここでreturnしてもf.Close()が実行される
        }
    }
    return string(result), nil // ここでreturnしてもf.Close()が実行される
}

重要なポイント:

  • defer f.Close()により、どこでreturnしても必ずファイルが閉じられる
  • エラー処理のreturnが複数あっても、すべてのパスでクリーンアップが実行される

3. deferの2つの利点

1. 確実性: リソース解放を忘れない 2. 可読性: 取得(Open)と解放(Close)が近い位置にある

4. 引数の評価タイミング

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

重要: defer実行時(ループの各回)にiの値が評価・保存される

  • 1回目:i=0が保存
  • 2回目:i=1が保存
  • 5回目:i=4が保存

5. LIFO(後入れ先出し)実行順序

複数のdeferがある場合、最後に登録されたものから順に実行されます。

上記の例では出力は:4 3 2 1 0

6. トレース機能の実装例

基本版:

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

func a() {
    trace("a")
    defer untrace("a")
    // 何らかの処理....
}

改良版(引数評価の活用):

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"))  // trace("a")はdefer実行時に評価される
    fmt.Println("in a")
}

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

func main() {
    b()
}

実行の流れ:

  1. b()開始
  2. defer un(trace("b"))実行 → trace("b")が即座に実行され”entering: b”出力、un("b")が遅延登録
  3. “in b”出力
  4. a()呼び出し
  5. defer un(trace("a"))実行 → trace("a")が即座に実行され”entering: a”出力、un("a")が遅延登録
  6. “in a”出力
  7. a()終了 → un("a")実行で”leaving: a”出力
  8. b()終了 → un("b")実行で”leaving: b”出力

7. deferの実用的な使用例

1. ファイル操作:

func processFile(filename string) error {
    file, err := os.Open(filename)
    if err != nil {
        return err
    }
    defer file.Close()
    
    // ファイル処理...
    return nil
}

2. Mutex(排他制御):

var mu sync.Mutex

func criticalSection() {
    mu.Lock()
    defer mu.Unlock()
    
    // クリティカルセクション...
}

3. パニック回復:

func safeFunction() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered:", r)
        }
    }()
    
    // 危険な処理...
}

8. 他言語との比較

他言語(try-finally、RAII等):

  • ブロックスコープでリソース管理
  • スコープを抜ける際に自動実行

Go(defer):

  • 関数スコープでリソース管理
  • 関数終了時に実行
  • より柔軟で明示的

deferは、Go言語独特の強力な機能であり、確実なリソース管理と読みやすいコードの両方を実現します。

おわりに 

本日は、Go言語を効果的に使うためのガイドラインについて解説しました。

よっしー
よっしー

何か質問や相談があれば、コメントをお願いします。また、エンジニア案件の相談にも随時対応していますので、お気軽にお問い合わせください。

それでは、また明日お会いしましょう(^^)

コメント

タイトルとURLをコピーしました