Go言語におけるSignalのハンドリングについて

golangには、Linuxのシグナルをハンドリングする機能がある。 具体的には、os/signalパッケージ/syscallパッケージとchannel機能を使う。

func main() {
    sigCh := make(chan os.Signal, 1)
    signal.Notify(sigCh,
        syscall.SIGTERM,
        syscall.SIGINT)

    switch(sigCh) {
        case syscall.SIGTERM:
            fmt.Println("SIGTERM")
        case syscall.SIGINT:
            fmt.Println("SIGINT")
    }
}

ただし、signalはlinux/unixにおける概念であり、WindowsでのCTRL^Cはキャッチできない。パッケージもsyscallだし。

Windowsに対応させるには、os.Interruptを使う (参考:signal - The Go Programming Language

func main() {
    sigCh := make(chan os.Signal, 1)
    signal.Notify(sigCh,
        syscall.SIGTERM,
        syscall.SIGINT,
        os.Intterupt)

    switch(sigCh) {
        case syscall.SIGTERM:
            fmt.Println("SIGTERM")
        case syscall.SIGINT:
            fmt.Println("SIGINT")
        case os.Interrupt:
            fmt.Println("Interrupt")
    }
}

これで対象のシグナルが発生すると、sigChにメッセージが通知されるようになる。 なお、sigChはchannelのため、読み出しを行うと、メッセージングされるまでブロッキングが発生する。

通常は何かを実行中のバックグランドでキャッチしたいはずなので、goroutineにする。ただし、goroutineはGCされないため、終了しないとリークしてしまうらしい(Go言語(Golang) はまりどころと解決策)。

なので、別途チャンネルを使って停止できるようにする。 このとき、ちゃんとgoroutineが終了するように、sync.WaitGroupを使って同期を行う

package main

import (
    "fmt"
    "os"
    "os/signal"
    "syscall"
    "sync"
)

func main() {
    wg := sync.WaitGroup{}
    doneCh := make(chan struct{}, 1)

    wg.Add(1)
    go func() {
        defer wg.Done()

        sigCh := make(chan os.Signal, 1)
        signal.Notify(sigCh,
            syscall.SIGTERM,
            syscall.SIGINT,
            os.Intterupt)
        defer signal.Stop(sigCh)

        for {
            select {
            case sig := <-sigCh:
                switch(sig) {
                case syscall.SIGTERM:
                    fmt.Println("SIGTERM")
                case syscall.SIGINT:
                    fmt.Println("SIGINT")
                case os.Interrupt:
                    fmt.Println("Interrupt")
                }
            case <-doneCh:
                fmt.Println("Done")
                return
            }
        }
    }()

    // 何かする

    // 終了
    doneCh <- struct{}{}
    wg.Wait()
}

context.Contextと組み合わせれば、CTRL^Cを受けてキャンセルを行ったりできる。

ただその場合、生成されたcontext.CancelFunc()を、シグナルハンドラのgoroutineで直接呼び出すか、一層channelをはさむべきなのか、今一つ判断がつかない。