26
14

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Go で "broken pipe" を無視したい僕達がハマる罠

Last updated at Posted at 2016-01-29

ぶろーくんぱいぷ?

 たとえば、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)
			}
		}
	}
}

 こんなふうにすれば良さそうですね!

chotto-matte.png それは罠よ!!

 えっ!?

$ 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 型の変数ですから、EPIPEerror 型として振る舞ってくれてくれている結果、コンパイルエラーにならずに通ってしまうんですね。

ちゃんと 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" が消えましたね!

disarmed.png

26
14
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
26
14

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?