これは TECHSCORE Advent Calendar 2017の5日目の記事です。
はじめに
今回はGoで「broken pipe」のエラーを判定して処理するようにした、という話です。
「golang broken pipe」で検索するととても素敵な記事が見つかりました。とても参考になりました。
背景
とあるGoで作ったWebアプリケーションのエラーメッセージが出ていました。
1 |
write tcp [::1]:3000->[::1]:65341: write: broken pipe |
エラーはレスポンスを返すところで起きているようです。
「broken pipe」でGoのソースコードを調べると、EPIPEのエラーメッセージであることがわかりました。
私の環境(Mac OS:darwin, ARCH:amd64)の場合は、以下で定義されています。
1 |
"broken pipe", |
syscall/zerrors_darwin_amd64.go#L1314
1 |
EPIPE = Errno(0x20) |
syscall/zerrors_darwin_amd64.go#L1217
試してみる
エラーを再現するために、以下のようなクライアントとサーバーを用意します。
クライアントは、リクエストを1秒でタイムアウトするようにしています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
// クライアントコード package main import ( "fmt" "io" "net/http" "os" "time" ) func main() { client := &http.Client{Timeout: 1 * time.Second} // 1秒でタイムアウト resp, err := client.Get("http://localhost:3000") if err != nil { fmt.Println(err) return } defer resp.Body.Close() _, err = io.Copy(os.Stdout, resp.Body) if err != nil { fmt.Println(err) return } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
// サーバーコード package main import ( "fmt" "log" "net" "net/http" "os" "syscall" ) func main() { log.Fatal(http.ListenAndServe(":3000", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { size := 100 * 1000 * 1000 _, err := w.Write(make([]byte, size)) // 適当なサイズのバイト書き込み(環境依存なのでうまくいかない場合はサイズを大きく増やしてみてください) if err != nil { fmt.Println(err) } }))) } |
用意したクライアントからサーバーに向けてリクエストを投げると、「broken pipe」エラーがでます。
1 2 3 4 5 6 7 |
http.ListenAndServe(":3000", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { size := 100 * 1000 * 1000 _, err := w.Write(make([]byte, size)) if err != nil { fmt.Println(err) // ここのエラーを拾っている } })) |
Goではerrorインターフェースというのがあり、エラーメッセージを表示するだけであればerrorの型を気にせずに処理を書けるようになっています。
1 2 3 |
type error interface { Error() string // errorインターフェースを満たす型はError()メソッドを持っているのでメッセージを出せる } |
しかし、今回のように特定のエラーのみ別の処理を当てたい場合には、errorを型アサーションしてチェックします。参考: Error handling and Go - The Go Blog
ということで、errorの型を調べてみます。ついでに構造も調べます。
1 2 3 4 5 6 7 8 9 10 |
http.ListenAndServe(":3000", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { size := 100 * 1000 * 1000 _, err := w.Write(make([]byte, size)) // 適当なサイズのバイト書き込み if err != nil { fmt.Println(err) fmt.Printf("%T\n", err) // 型調べ fmt.Printf("%#v\n", err) // 構造調べ return } })) |
結果は以下の通りです。
1 2 3 |
write tcp [::1]:3000->[::1]:56964: write: broken pipe *net.OpError &net.OpError{Op:"write", Net:"tcp", Source:(*net.TCPAddr)(0xc420018e10), Addr:(*net.TCPAddr)(0xc420018e40), Err:(*os.SyscallError)(0xc42000c200)} |
これで*net.OpErrorというのがわかりました。
1 2 3 4 5 6 7 8 |
// netパッケージ type OpError struct Op string Net string Source *net.TCPAddr Addr *net.TCPAddr Err *os.SyscallError } |
ここでさらに、*os.SyscallErrorがでてきました。
1 2 3 4 5 |
// osパッケージ type SyscallError struct { Syscall string Err error } |
というわけで、2段階でアサーションして、syscall.EPIPEか判定をしたコードが以下です。
これで当初の目的だった「EPIPEに対して別の処理を当てる」ことができるようになりました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
log.Fatal(http.ListenAndServe(":3000", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { size := 1000 * 1000 * 100 _, err := w.Write(make([]byte, size)) if err != nil { fmt.Println(err) if opErr, ok := err.(*net.OpError); ok { if sysErr, okok := opErr.Err.(*os.SyscallError); okok && sysErr.Err == syscall.EPIPE { fmt.Println("EPIPE ERROR") // EPIPE return } } fmt.Println("NOT EPIPE ERROR") // EPIPE以外 } }))) |
まとめ
書き込みが始まっているタイミングでクライアントがコネクションを切ると起こってしまうんですが、
もっとうまいやりかた(検証方法と解決方法)をご存知の方は教えていただけるとうれしいです。
最近 Goならわかるシステムプログラミングを読み始めたのですが、とても勉強になるので興味がある方はぜひ読んでみてください。