signal.Notify したチャンネルを close すると死ぬ(可能性がある)

TL; DR

signal.Stop しよう

https://golang.org/pkg/os/signal/#Stop

検証

ダメな例

package main

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

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

    fmt.Println("C-c plz")
    time.Sleep(10 * time.Second)
}
$ go run main.go
C-c plz
^Cpanic: send on closed channel

goroutine 5 [running]:
os/signal.process(0x10dbc60, 0xc00002aa30)
        /usr/local/Cellar/go/1.11.1/libexec/src/os/signal/signal.go:227 +0x163
os/signal.loop()
        /usr/local/Cellar/go/1.11.1/libexec/src/os/signal/signal_unix.go:23 +0x52
created by os/signal.init.0
        /usr/local/Cellar/go/1.11.1/libexec/src/os/signal/signal_unix.go:29 +0x41
exit status 2

ある channel がクローズ済かは送信者はわからないので仕方がない。

大丈夫な例

package main

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

func main() {
    c := make(chan os.Signal, 1)
    signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
    signal.Stop(c) // signal.Stop を追加した
    close(c)

    fmt.Println("C-c plz")
    time.Sleep(10 * time.Second)
}
$ go run main.go
C-c plz
^Csignal: interrupt

OK

背景

graceful restart 的なことをするときに、シグナルでハンドリングをしていた。のだが、場合によっては即座に死んでほしくないパターンがあった。API通信相手がメンテ中だとわかっている場合とかは即死すると即死→メンテ→即死→メンテの無限ループって怖くね?状態になる。

しょうがないのでメンテのときはある程度待ってから死ぬようにしてみるか、と思って書いているわけだが、シグナルハンドリングが終わったあとの息が長くなるとリソースの解放漏れが気になってきた。そこでさくっと close しようとしたら、これ死ぬんじゃね?って思って調べて今に至る。

ここまで書いた後でさっさと死んで起動直後にメンテ中か確認すれば特に悩む必要なかったなって思った。おしまい。