DRYな備忘録

Don't Repeat Yourself.

【Go言語】Ctrl+cなどによるSIGINTの捕捉とdeferの実行

問題

deferを使って後処理をしたい場合に、プロセスがCtrl+cなどSIGINTで中断されるとdeferしたものが発火しない。プロセス自体が中断されるのであたりまえなんだけども。

問題の再現

package main

import (
    "fmt"
    "time"
)

func main() {

    defer teardown()

    hoge()

    fmt.Println("終了")

}

func hoge() {
    for i := 0; i < 5; i++ {
        fmt.Println(i)
        time.Sleep(1 * time.Second)
    }
}

func teardown() {
    fmt.Println("データのあとかたづけ")
}

上記の実行

% go run main.go
0
1
2
3
4
終了
データのあとかたづけ
%

SIGINTの場合

% go run main.go
0
1
2
3
^Csignal: interrupt
%
# 「データのあとかたづけ」が発火しない

SIGINTの捕捉

package main

import (
    "fmt"
    "os"
    "os/signal"
    "time"
)

func main() {

    defer teardown()

    // {{{ ここから
    // SIGINTを待ち受けるchanを作成
    c := make(chan os.Signal, 1)
    // それを登録
    signal.Notify(c, os.Interrupt)
    // chanからの通知を受けるgoroutineを起動
    go func() {
        for sig := range c {
            fmt.Println("シグナル来た", sig)
            close(c)
            // SIGINTをchanで吸収しちゃってるので、
            // 明示的にExitする必要がある。
            os.Exit(130)
        }
    }()
    // }}} ここまで追加

    hoge()

    fmt.Println("終了")

}

func hoge() {
    for i := 0; i < 5; i++ {
        fmt.Println(i)
        time.Sleep(1 * time.Second)
    }
}

func teardown() {
    fmt.Println("データのあとかたづけ")
}

os.Exitを使い明示的にプロセスを終了しており、このままではdeferしたteardownは呼ばれないので、以下の行を追加した。

                for sig := range c {
                        fmt.Println("シグナル来た", sig)
                        close(c)
+                       teardown()
                        // SIGINTをchanで吸収しちゃってるので、
                        // 明示的にExitする必要がある。
                        os.Exit(130)

実行してCtrl+cすると、

% go run main.go
0
1
2
^Cシグナル来た interrupt
データのあとかたづけ
exit status 130
%

となっていい感じ。

deferのためのリファクタリング

teardownの呼び出しが2箇所に書かなきゃいけないのはあまり筋がよくないので、defer一発でどうにかしたほうがシュッとすると思う。「hogeが終わった」というメッセージを伝達するdoneというようなチャンネルを経由して、SIGINTを受けるチャンネルと並列で扱うのがいいんではないか。終了コードが0になってしまうが、まあいっか。

package main

import (
    "fmt"
    "os"
    "os/signal"
    "time"
)

func main() {

    defer teardown()

    c := make(chan os.Signal, 1)
    signal.Notify(c, os.Interrupt)

    done := make(chan error, 1)
    go hoge(done)

    select {
    case sig := <-c:
        fmt.Println("シグナル来た:", sig)
        /*
         teardown中に再度SIGINTが来る場合を考慮し、
         send on closed channelのpanicを避ける。
       */
        // close(c)
        return
    case err := <-done:
        fmt.Println("hogeの終了:", err)
    }

    fmt.Println("終了")

}

func hoge(done chan<- error) {
    for i := 0; i < 5; i++ {
        fmt.Println(i)
        time.Sleep(1 * time.Second)
    }
    done <- nil
    close(done)
}

func teardown() {
    fmt.Println("データのあとかたづけ")
}

非常にGoっぽいコードになった。

参考

DRYな備忘録として

プログラミング言語Go (ADDISON-WESLEY PROFESSIONAL COMPUTING SERIES)

プログラミング言語Go (ADDISON-WESLEY PROFESSIONAL COMPUTING SERIES)