6
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Go】syncパッケージの最近のアップデートで goroutine 内の panic が伝播するようになった話(追記あり)

Last updated at Posted at 2025-07-14

はじめに

(2025/07/24更新)
この記事で紹介している「errgroup.GroupWait()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 の挙動に基づいたものですので、現在のバージョンとは異なる点にご注意ください :cry:

こんにちは、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が発生した場合、以下のような挙動でした。

1_panic_propagation/main.go
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内で発生したpanicGroup.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() も伝播されるようになりました。

2_goexit_propagation/main.go
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 が伝播するようになったことでより柔軟で堅牢なエラーハンドリングが可能になりました。
高可用性が求められるシステムにとって、これは非常に価値のあるアップデートではないでしょうか..!

最後にこの記事で用いたサンプルコードをおいておきます。

間違いなどありましたら、コメントにてご指摘ください。ここまで読んでいただき、ありがとうございました!

6
1
0

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
6
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?