この記事は Go3 Advent Calendar 2019 13日目の記事です qiita.com
TL;DR
x/sync/errgroup
はいいぞ
本編
最近よく書くサーバの起動部分のコードを紹介します
分かる人には見るのが一番早いと思うので、早速コード全体を載せます
やりたいことは
localhost:8888
で HTTP サーバを起動- SIGINT を受けると HTTP サーバを graceful shutdown
- 各所での
ctx.Done()
のハンドリング
これらを混乱しないように記述したかったのが最初のモチベーションです
package main import ( "context" "fmt" "net/http" "os" "x/sync/errgroup" ) func main() { os.Exit(run(context.Background())) } func run(ctx context.Context) int { var eg *errgroup.Group eg, ctx = errgroup.WithContext(ctx) eg.Go(func() error { return runServer(ctx) }) eg.Go(func() error { return signal(ctx) }) eg.Go(func() error { <-ctx.Done() return ctx.Err() }) if err := eg.Wait(); err != nil { fmt.Println(err) return 1 } return 0 } func signal(ctx context.Context) error { sigCh := make(chan os.Signal, 1) signal.Notify(sigCh, os.Interrupt) select { case <-ctx.Done(): signal.Reset() return nil case sig := <-sigCh: return fmt.Errorf("signal received: %v", sig.String()) } } func runServer(ctx context.Context) error { s := &http.Server{ Addr: ":8888", Handler: nil, } errCh := make(chan error) go func() { defer close(errCh) if err := s.ListenAndServe(":8888", nil); err != nil { errCh <- err } }() select { case <-ctx.Done(): return s.Shutdown() case err := <-errCh: return err } }
解説
x/sync/errgroup
単純に一つ HTTP サーバを立てるだけであれば必要ありませんが、今回のように HTTP サーバも起動したいし、シグナルハンドリングもしたい、なんなら他のサーバも起動したい、かつそれらの error 処理も纏めてやりたいというような要件を達成するために x/sync/errgroup
は非常に便利なパッケージです
指定した goroutine をグループ化し、そのエラー処理を取り纏めてくれます
以下のように使います
errgroup.Group
型の値(0値が使用可能)を用意するerrgroup.Group.Go()
で error を返す関数を goroutine として起動するerrgroup.Group.Wait()
でerrgroup.Group.Go()
で起動した goroutine の全てが終了するまで待機し、一番最初に返ってきた non-nil error を返す
一番最初に返ってきた non-nil error を強調しましたが、どうしても起動した goroutine の全ての error を補足したいときは x/sync/errgroup
は使用できないことに注意してください
// この場合は sync.WaitGroup
などで自力で頑張りましょう
加えて errgroup.WithContext()
で初期化をしておくと、最初に non-nil error が返ってきた瞬間に context がキャンセルされます
今回のコードでは run()
がこの処理をしている部分です
runServer()
が error を返すsignal()
が error を返す
のどちらかが起きると、ctx のキャンセル処理により他方も終了処理が出来る、という作りです
func run(ctx context.Context) int { var eg *errgroup.Group eg, ctx = errgroup.WithContext(ctx) eg.Go(func() error { return runServer(ctx) }) eg.Go(func() error { return signal(ctx) }) eg.Go(func() error { <-ctx.Done() return ctx.Err() }) if err := eg.Wait(); err != nil { fmt.Println(err) return 1 } return 0 }
runServer()
/signal()
次は errgroup.Group.Go()
で呼び出される側の関数です
単純に http.ListenAndServe()
を実行するだけでもよいですが、ctx のキャンセル待受処理を両立するために http.ListenAndServe()
の error を chan 経由でやりとりします
func runServer(ctx context.Context) error { // サーバ初期化 s := &http.Server{Addr: ":8888", Handler: nil} // error 伝達用 chan errCh := make(chan error) go func() { defer close(errCh) if err := s.ListenAndServe(":8888", nil); err != nil { // error を chan 経由で伝える errCh <- err } }() select { case <-ctx.Done(): // 上位で ctx のキャンセルされた場合は graceful shutdown return s.Shutdown() case err := <-errCh: // `s.ListenAndServe()` が error を返した場合、そのまま error を返す return err } }
シグナル待受処理も同様です シグナルを受ける chan
と ctx.Done()
を select
文で待ち受けるように書きます
2019/12/13 追記: 取りこぼす可能性があるので、chan
にバッファを付けた
@tnnsh1 signal.Notifyにわたすチャネルはバッファつけたほうがいいですね。
> the caller must ensure that c has sufficient buffer space to keep up with the expected signal rate.https://golang.org/pkg/os/signal/#Notify …https://tennashi.hatenablog.com/entry/2019/12/13/010938 …
func signal(ctx context.Context) error { sigCh := make(chan os.Signal, 1) signal.Notify(sigCh, os.Interrupt) select { case <-ctx.Done(): signal.Reset() return nil case sig := <-sigCh: return fmt.Errorf("signal received: %v", sig.String()) } }
やってみて
シグナルハンドリングも x/sync/errgroup
にのせることで、制御フローをシンプルにできてるのではないかなぁと感じています
私が実際に導入した要件では、HTTP サーバ、gRPC サーバが一つのコードで動いている中、追加で別ポートに gRPC サーバを立てる必要が出てきました
その際もコードの拡張は eg.Go()
部分と runServer()
部分の追加でよく、gRPC サーバ自身の後処理も runServer()
で完結できているので苦労なく追加することができました
x/sync/errgroup
便利