はじめに
(2025/07/24更新)
この記事で紹介している「errgroup.Group
の Wait()
で panic
が呼び出し元に伝播する」という挙動は、golang.org/x/sync
パッケージの v0.14.0 で導入されたものでした。
しかしその後、v0.16.0 のリリースでこの仕様は取り消されました。
v0.16.0: errgroup: revert propagation of panics
そのため、2025年7月24日現在の最新版を使用しても、panic
は呼び出し元には伝播しません。
記事内の内容はあくまで v0.14.0 ~ v0.15.0 の挙動に基づいたものですので、現在のバージョンとは異なる点にご注意ください
こんにちは、kenです。お仕事ではGoを書いています。
最近、golang.org/x/sync
パッケージの ChangeLog を見ていると、興味深い内容があったのでこの記事ではそれを紹介しようと思います
それはズバリ、panic
値がGroup.Wait()
を通じて呼び出し元へ伝播されるようになったことです。
先に今回この記事で紹介する予定の golang.org/x/sync
のChangeLog(v0.13.0 → v0.14.0) を貼っておきます。
errgroup.Group
とは
まず簡単におさらいです。golang.org/x/sync/errgroupは、複数のgoroutineを同期させつつ、エラーハンドリングを簡潔に行えるライブラリです。
標準のsync.WaitGroup
と似ていますが、エラーを返すgoroutineを扱えるというのが大きな違いです。
package main
import (
"fmt"
"net/http"
"golang.org/x/sync/errgroup"
)
func main() {
g := new(errgroup.Group)
urls := []string{
"https://example1.com",
"https://example2.com",
"https://example3.com",
}
for _, url := range urls {
g.Go(func() error {
resp, err := http.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
return nil
})
}
if err := g.Wait(); err != nil {
fmt.Printf("Error occurred: %v\n", err)
} else {
fmt.Println("All requests succeeded!")
}
}
このように、複数のHTTPリクエストを並行実行し、いずれかでエラーが発生した場合にそれを捕捉できるわけですね。
sync.WaitGroup
も実行した goroutine が終わるのを待つものの、エラーがあったかはわかりません。一方で errgroup.Wait
は並行実行内でエラーがあればその最初のエラーを返してくれます。
golang.org/x/sync/errgroup
の最近のアップデート
冒頭にも書きましたが、今回記事で取り上げたいのはgolang.org/x/sync/errgroup
の最近のアップデートにより、panic
も伝播するようになったということです。
アプデ前と後を比較しながら詳しく説明します。
従来の挙動:panicが発生すると...
これまでのerrgroup.Group
では、goroutine内でpanic
が発生した場合、以下のような挙動でした。
package main
import (
"fmt"
"golang.org/x/sync/errgroup"
)
func main() {
defer func() {
if r := recover(); r != nil {
fmt.Printf("Recovered from panic: %v\n", r)
}
}()
g := new(errgroup.Group)
g.Go(func() error {
fmt.Println("Normal task completed")
return nil
})
g.Go(func() error {
panic("Something went wrong!")
})
if err := g.Wait(); err != nil {
fmt.Printf("Error: %v\n", err)
}
fmt.Println("This line is now reached with v0.14.0!")
}
実行結果(v0.13.0以前):
panic: Something went wrong!
goroutine 4 [running]:
main.main.func3()
/Users/kenjihashimoto/go/src/github.com/TechBlogSamples/go-sync-panic-propagate/02_traditional_panic/main.go:24 +0x2c
golang.org/x/sync/errgroup.(*Group).Go.func1()
/Users/kenjihashimoto/go/pkg/mod/golang.org/x/sync@v0.13.0/errgroup/errgroup.go:79 +0x54
created by golang.org/x/sync/errgroup.(*Group).Go in goroutine 1
/Users/kenjihashimoto/go/pkg/mod/golang.org/x/sync@v0.13.0/errgroup/errgroup.go:76 +0x94
exit status 2
Exit code: 1
このように、並行実行の際にpanic
が発生すると、panic発生と同時にプログラム全体が停止してしまい、呼び出し元でrecover
を使っても捕捉できませんでした。
v0.14.0での変更:panicが伝播するように!
しかしv0.14.0からは、goroutine内で発生したpanic
がGroup.Wait()
を通じて呼び出し元へ伝播されるようになりました!同じコードを最新版で実行してみます。
実行結果(v0.14.0以降):
Normal task completed
Recovered from panic: recovered from errgroup.Group: Something went wrong!
goroutine 22 [running]:
runtime/debug.Stack()
/opt/homebrew/Cellar/go/1.24.4/libexec/src/runtime/debug/stack.go:26 +0x64
golang.org/x/sync/errgroup.(*Group).add.func1.1()
/Users/kenjihashimoto/go/pkg/mod/golang.org/x/sync@v0.14.0/errgroup/errgroup.go:124 +0x164
panic({0x1050c1ea0?, 0x1050dc600?})
/opt/homebrew/Cellar/go/1.24.4/libexec/src/runtime/panic.go:792 +0x124
main.main.func3()
/Users/kenjihashimoto/go/src/github.com/TechBlogSamples/go-sync-panic-propagate/02_traditional_panic/main.go:24 +0x2c
golang.org/x/sync/errgroup.(*Group).add.func1()
/Users/kenjihashimoto/go/pkg/mod/golang.org/x/sync@v0.14.0/errgroup/errgroup.go:130 +0x88
created by golang.org/x/sync/errgroup.(*Group).add in goroutine 1
/Users/kenjihashimoto/go/pkg/mod/golang.org/x/sync@v0.14.0/errgroup/errgroup.go:98 +0x80
Exit code: 0
Recovered from panic
という出力にもあるように、panic
がきちんとRecover
できています! プログラムが異常終了することなく、panic
を適切にハンドリングできるようになりました。
v0.14.0での変更:runtime.Goexitの伝播
ちなみに、v0.14.0ではpanic
だけでなく runtime.Goexit()
も伝播されるようになりました。
package main
import (
"fmt"
"runtime"
"golang.org/x/sync/errgroup"
)
func main() {
defer func() {
fmt.Println("Main function is finishing")
}()
g := new(errgroup.Group)
g.Go(func() error {
fmt.Println("Task 1 completed")
return nil
})
g.Go(func() error {
fmt.Println("Task 2 calling Goexit")
runtime.Goexit() // goroutineを強制終了
return nil
})
fmt.Println("Before Wait()")
g.Wait()
fmt.Println("After Wait() - this line is never reached")
}
実行結果:
Before Wait()
Task 1 completed
Task 2 calling Goexit
Main function is finishing
fatal error: no goroutines (main called runtime.Goexit) - deadlock!
exit status 2
runtime.Goexit()
が呼ばれると、g.Wait()
も同様にruntime.Goexit()
を呼び出すため、メイン関数も終了しています。
さいごに
golang.org/x/sync
パッケージの v0.14.0 で panic
が伝播するようになったことでより柔軟で堅牢なエラーハンドリングが可能になりました。
高可用性が求められるシステムにとって、これは非常に価値のあるアップデートではないでしょうか..!
最後にこの記事で用いたサンプルコードをおいておきます。
間違いなどありましたら、コメントにてご指摘ください。ここまで読んでいただき、ありがとうございました!