Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .golangci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ linters:
- fatcontext
- gocheckcompilerdirectives
- gocognit
- goconst
- gocritic
- gomodguard_v2
- goprintffuncname
Expand Down
36 changes: 22 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
[![Go Reference](https://pkg.go.dev/badge/github.com/min0625/errgroup.svg)](https://pkg.go.dev/github.com/min0625/errgroup)
[![codecov](https://codecov.io/gh/min0625/errgroup/branch/main/graph/badge.svg)](https://codecov.io/gh/min0625/errgroup)

A recoverable errgroup based on `golang.org/x/sync/errgroup` that can recover from panics. Panics are caught and re-panicked in the `Wait` call.
**English** | [繁體中文](README.zh-TW.md)

A drop-in `golang.org/x/sync/errgroup` replacement that recovers panics in goroutines and re-panics them inside the `Wait` call.

Ref: https://github.com/golang/go/issues/53757

Expand All @@ -18,21 +20,17 @@ Ref: https://github.com/golang/go/issues/53757
go get github.com/min0625/errgroup
```

## Panic behaviour

Panics in goroutines started by `Go` or `TryGo` are caught and re-panicked inside `Wait`, wrapped as:

- `PanicError` — when the panicked value implements `error`
- `PanicValue` — for all other values

Both types expose a `Stack` field containing the stack trace captured at the point of the panic.

If multiple goroutines panic concurrently, only the first panic is propagated; the rest are silently discarded.

## Example
```go
package main

import (
"fmt"

func Example() {
"github.com/min0625/errgroup"
)

func main() {
// This case uses "github.com/min0625/errgroup" which will catch panics.
// If you import "golang.org/x/sync/errgroup" instead, it won't catch panics.
// You can try this in the Go Playground: https://go.dev/play/p/7pUX6uQ2mCH
Expand Down Expand Up @@ -67,5 +65,15 @@ func Example() {

// Output: oops
}

```

## Panic behaviour

Panics in goroutines started by `Go` or `TryGo` are caught and re-panicked inside `Wait`, wrapped as:

- `PanicError` — when the panicked value implements `error`
- `PanicValue` — for all other values

Both types expose a `Stack` field containing the stack trace captured at the point of the panic.

If multiple goroutines panic concurrently, only the first panic is propagated; the rest are silently discarded.
79 changes: 79 additions & 0 deletions README.zh-TW.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# errgroup
[![Go Reference](https://pkg.go.dev/badge/github.com/min0625/errgroup.svg)](https://pkg.go.dev/github.com/min0625/errgroup)
[![codecov](https://codecov.io/gh/min0625/errgroup/branch/main/graph/badge.svg)](https://codecov.io/gh/min0625/errgroup)

[English](README.md) | **繁體中文**

可直接替換 `golang.org/x/sync/errgroup` 的套件,會回收 goroutine 中的 panic,並在 `Wait` 呼叫中重新拋出。

參考:https://github.com/golang/go/issues/53757

## 套件

| 套件 | 說明 |
|---|---|
| `github.com/min0625/errgroup` | `golang.org/x/sync/errgroup` 的直接替換版,額外提供 panic 回收 |
| [`github.com/min0625/errgroup/x/errgroup`](x/errgroup/README.zh-TW.md) | 情境感知(context-aware)變體 — 直接將 `context.Context` 傳入每個 goroutine 函式 |

## 安裝
```sh
go get github.com/min0625/errgroup
```

## 範例
```go
package main

import (
"fmt"

"github.com/min0625/errgroup"
)

func main() {
// 此範例使用 "github.com/min0625/errgroup",它會捕捉 panic。
// 若改為匯入 "golang.org/x/sync/errgroup",則不會捕捉 panic。
// 你可以在 Go Playground 試玩:https://go.dev/play/p/7pUX6uQ2mCH
var g errgroup.Group

defer func() {
// 會捕捉到 panic。
if p := recover(); p != nil {
switch t := p.(type) {
case errgroup.PanicValue:
fmt.Println(t.Recovered)
case errgroup.PanicError:
fmt.Println(t.Recovered)
}
}
}()

g.Go(func() error {
// 做些事情
return nil
})

g.Go(func() error {
panic("oops")
})

if err := g.Wait(); err != nil {
// 處理錯誤
fmt.Println(err)
return
}

// Output: oops
}
```

## Panic 行為

由 `Go` 或 `TryGo` 啟動的 goroutine 若發生 panic,會被捕捉並在 `Wait` 中重新拋出,並包裝為:

- `PanicError` — 當 panic 的值實作了 `error` 介面時
- `PanicValue` — 其他所有情況

兩種型別都會提供 `Stack` 欄位,內含 panic 發生當下所擷取的堆疊追蹤(stack trace)。

若有多個 goroutine 同時 panic,只有第一個 panic 會被傳播,其餘會被靜默丟棄。
101 changes: 101 additions & 0 deletions errgroup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,107 @@ func Test_Group_TryGo(t *testing.T) {
require.NoError(t, g.Wait())
}

func Test_Group_PanicError_Unwrap(t *testing.T) {
t.Parallel()

sentinel := errors.New("oops")

var g errgroup.Group

g.Go(func() error {
panic(sentinel)
})

defer func() {
p := recover()
require.NotNil(t, p)

err, ok := p.(error)
require.True(t, ok)

// PanicError.Unwrap exposes the original error to errors.Is/As.
require.ErrorIs(t, err, sentinel)

var pe errgroup.PanicError
require.ErrorAs(t, err, &pe)
assert.Equal(t, sentinel, pe.Recovered)
}()

_ = g.Wait()
}

func Test_Group_Panic_AlreadyPanicError(t *testing.T) {
t.Parallel()

orig := errgroup.PanicError{
Recovered: errors.New("oops"),
Stack: []byte("original stack"),
}

var g errgroup.Group

g.Go(func() error {
panic(orig)
})

defer func() {
p := recover()
require.NotNil(t, p)

// An already-wrapped PanicError is propagated unchanged, not
// re-wrapped (the original Stack is preserved).
pe, ok := p.(errgroup.PanicError)
require.True(t, ok)
assert.Equal(t, orig, pe)
}()

_ = g.Wait()
}

func Test_Group_Panic_AlreadyPanicValue(t *testing.T) {
t.Parallel()

orig := errgroup.PanicValue{
Recovered: "oops",
Stack: []byte("original stack"),
}

var g errgroup.Group

g.Go(func() error {
panic(orig)
})

defer func() {
p := recover()
require.NotNil(t, p)

// An already-wrapped PanicValue is propagated unchanged, not
// re-wrapped (the original Stack is preserved).
pv, ok := p.(errgroup.PanicValue)
require.True(t, ok)
assert.Equal(t, orig, pv)
}()

_ = g.Wait()
}

func Test_PanicValue_String_WithoutStack(t *testing.T) {
t.Parallel()

pv := errgroup.PanicValue{Recovered: "oops"}

assert.Equal(t, "recovered from errgroup.Group: oops", pv.String())
}

func Test_PanicValue_String_WithStack(t *testing.T) {
t.Parallel()

pv := errgroup.PanicValue{Recovered: "oops", Stack: []byte("the stack")}

assert.Equal(t, "recovered from errgroup.Group: oops\nthe stack", pv.String())
}

func Test_Group_TryGo_Panic(t *testing.T) {
t.Parallel()

Expand Down
13 changes: 12 additions & 1 deletion x/errgroup/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
[![Go Reference](https://pkg.go.dev/badge/github.com/min0625/errgroup/x/errgroup.svg)](https://pkg.go.dev/github.com/min0625/errgroup/x/errgroup)
[![codecov](https://codecov.io/gh/min0625/errgroup/branch/main/graph/badge.svg)](https://codecov.io/gh/min0625/errgroup)

**English** | [繁體中文](README.zh-TW.md)

A context-aware variant of [`github.com/min0625/errgroup`](../../README.md) that passes a derived `context.Context` directly into each goroutine function, eliminating the need to capture the context via closure.

## Differences from the root package
Expand All @@ -22,7 +24,16 @@ go get github.com/min0625/errgroup/x/errgroup
## Example

```go
func Example() {
package main

import (
"context"
"fmt"

"github.com/min0625/errgroup/x/errgroup"
)

func main() {
// This case uses "github.com/min0625/errgroup/x/errgroup" which will catch panics.
// If you import "golang.org/x/sync/errgroup" instead, it won't catch panics.
var g errgroup.Group
Expand Down
85 changes: 85 additions & 0 deletions x/errgroup/README.zh-TW.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# errgroup/x/errgroup
[![Go Reference](https://pkg.go.dev/badge/github.com/min0625/errgroup/x/errgroup.svg)](https://pkg.go.dev/github.com/min0625/errgroup/x/errgroup)
[![codecov](https://codecov.io/gh/min0625/errgroup/branch/main/graph/badge.svg)](https://codecov.io/gh/min0625/errgroup)

[English](README.md) | **繁體中文**

[`github.com/min0625/errgroup`](../../README.zh-TW.md) 的情境感知(context-aware)變體,會將衍生的 `context.Context` 直接傳入每個 goroutine 函式,免去透過閉包(closure)擷取 context 的需要。

## 與根套件的差異

| | `github.com/min0625/errgroup` | `github.com/min0625/errgroup/x/errgroup` |
|---|---|---|
| Goroutine 函式簽章 | `func() error` | `func(context.Context) error` |
| 取得 context | 透過閉包擷取 | 以參數傳入 |
| 建構子 | `WithContext(ctx)` 回傳 `(*Group, context.Context)` | `New(ctx)` 回傳 `*Group` |
| 零值 | 有效,無 context 取消機制 | 有效,使用 `context.Background()` |

## 安裝

```sh
go get github.com/min0625/errgroup/x/errgroup
```

## 範例

```go
package main

import (
"context"
"fmt"

"github.com/min0625/errgroup/x/errgroup"
)

func main() {
// 此範例使用 "github.com/min0625/errgroup/x/errgroup",它會捕捉 panic。
// 若改為匯入 "golang.org/x/sync/errgroup",則不會捕捉 panic。
var g errgroup.Group

defer func() {
// 會捕捉到 panic。
if p := recover(); p != nil {
switch t := p.(type) {
case errgroup.PanicValue:
fmt.Println(t.Recovered)
case errgroup.PanicError:
fmt.Println(t.Recovered)
}
}
}()

g.Go(func(_ context.Context) error {
// 做些事情
return nil
})

g.Go(func(_ context.Context) error {
panic("oops")
})

if err := g.Wait(); err != nil {
// 處理錯誤
fmt.Println(err)
return
}

// Output: oops
}
```

## Panic 行為

由 `Go` 或 `TryGo` 啟動的 goroutine 若發生 panic,會被捕捉並在 `Wait` 中重新拋出,並包裝為:

- `PanicError` — 當 panic 的值實作了 `error` 介面時
- `PanicValue` — 其他所有情況

兩種型別都會提供 `Stack` 欄位,內含 panic 發生當下所擷取的堆疊追蹤(stack trace)。

若有多個 goroutine 同時 panic,只有第一個 panic 會被傳播,其餘會被靜默丟棄。

## 零值注意事項

零值的 `Group`(未透過 `New` 建立)是有效的,並會在第一次呼叫 `Go`、`TryGo`、`SetLimit` 或 `Wait` 時自動初始化。但其基礎 context 會固定為 `context.Background()`。若要使用自訂的可取消 context,請務必以 `New(ctx)` 建立群組。
Loading