ぶろーくんぱいぷ?
たとえば、Goでこんなプログラムを書いたとして、
// main.go
package main
import "fmt"
func main() {
for {
fmt.Println("Wow!")
}
}
これを head
コマンドにパイプすると、
$ go run main.go |head
Wow!
Wow!
Wow!
Wow!
Wow!
Wow!
Wow!
Wow!
Wow!
Wow!
signal: broken pipe
最後になぜか怒られるというアレですね。
この場合、main.go は無限に「Wow!」を書こうとしているわけですが、head
は最初の10行を表示すると「俺ができるのはここまでだぜ」と言ってお亡くなりになってしまい、結果的に main.go の行き場のなくなったヤル気が「signal: broken pipe」という怒りとなって現れるという寸法です。
このままでも特に実害はないのですが、やはりエラーメッセージが表示されるというのはいかにもカッコがつきませんよね。
というわけで、"broken pipe" を無視してきれいに終われるようにしましょう。
EPIPE
を捕捉する
やり方は簡単です。fmt.Println
の戻り値をちゃんと確認しましょう:
n, err := fmt.Println("Wow!")
この err
が "broken pipe" を示す値だったら、エラーを無視します。さて、どうやって "broken pipe" と判別すれば良いのでしょうか?
golang.org とかで検索すると、syscall.EPIPE
という値がありますね。なるほど、LinuxのMan pageを見ても、EPIPE
というのは閉じられているパイプに書き込んでしまった際に返されるエラー値のようです。
では、これを使って、
// main.go
package main
import (
"fmt"
"syscall"
)
func main() {
for {
_, err := fmt.Println("Wow!")
if err != nil {
if err == syscall.EPIPE {
break
} else {
panic(err)
}
}
}
}
こんなふうにすれば良さそうですね!
えっ!?
$ go run main.go |head
Wow!
Wow!
Wow!
Wow!
Wow!
Wow!
Wow!
Wow!
Wow!
Wow!
panic: write /dev/stdout: broken pipe
goroutine 16 [running]:
runtime.panic(0x4aec20, 0xc208022180)
/usr/lib/go/src/pkg/runtime/panic.c:279 +0xf5
main.main()
/gocode/pipe/main.go:15 +0x170
goroutine 17 [runnable]:
runtime.MHeap_Scavenger()
/usr/lib/go/src/pkg/runtime/mheap.c:507
runtime.goexit()
/usr/lib/go/src/pkg/runtime/proc.c:1445
goroutine 18 [runnable]:
bgsweep()
/usr/lib/go/src/pkg/runtime/mgc0.c:1976
runtime.goexit()
/usr/lib/go/src/pkg/runtime/proc.c:1445
goroutine 19 [runnable]:
runfinq()
/usr/lib/go/src/pkg/runtime/mgc0.c:2606
runtime.goexit()
/usr/lib/go/src/pkg/runtime/proc.c:1445
exit status 2
なんじゃこりゃーー
DISARM THE TRAP
どうして EPIPE
で "broken pipe" が捕捉できなかったのでしょう?
原因はそれぞれの型を表示してみればわかります:
// main.go
package main
import (
"fmt"
"os"
"syscall"
)
func main() {
for {
_, err := fmt.Println("Wow!")
if err != nil {
fmt.Fprintf(os.Stderr, "err : %T\n", err)
fmt.Fprintf(os.Stderr, "EPIPE: %T\n", syscall.EPIPE)
break
}
}
}
実行結果:
$ go run main.go |head -n 1
Wow!
err : *os.PathError
EPIPE: syscall.Errno
見事に型が違いますね。
あれ?でも、Go言語では型が違うものどうしを比較したらコンパイル時に型エラーになるんじゃなかったでしたっけ?
※追記:これがコンパイルが通ってしまう理由ではなかったようです。詳細は↓のコメント欄を参照
これがこの罠の罪深いところ。EPIPE
は untyped constant として定義されている ため、それ自体では 型付けされていない のです。
言ってる意味がわからないという方には、あとで Robさんのブログポスト を読んでもらうこととして、とりあえずここでは EPIPE
はソースコード中で書かれた場所によって "いい感じに" 型を合わせてくれるのだと思っておいてください。
ここでは比較の対象が error
型の変数ですから、EPIPE
が error
型として振る舞ってくれてくれている結果、コンパイルエラーにならずに通ってしまうんですね。
ちゃんと EPIPE
を捕捉する
結局、どうするべきだったのでしょう?
Println
が返すエラーの型である *os.PathError
は、その原因となったエラーの値を持っていますので、それが EPIPE
なのかを確認すれば OK です:
// main.go
package main
import (
"fmt"
"os"
"syscall"
)
func main() {
for {
_, err := fmt.Println("Wow!")
if err != nil {
if e, ok := err.(*os.PathError); ok && e.Err == syscall.EPIPE {
break
} else {
panic(err)
}
}
}
}
型アサーションを書くのがちょっと面倒ですが、仕方がないですね。
実行すると:
go run main.go |head
Wow!
Wow!
Wow!
Wow!
Wow!
Wow!
Wow!
Wow!
Wow!
Wow!
おめでとうございます!見事に "broken pipe" が消えましたね!