Documentation
¶
Overview ¶
Package scope provides scope-bound structured concurrency primitives for Go.
A Scope binds the lifetime of spawned goroutines to a lexical block, so that the goroutines are guaranteed to complete before the block exits. This avoids goroutine leaks caused by missing wg.Wait() calls, unobserved panics, and errors that escape unhandled.
The primary entry point is Run, which establishes a scope and invokes a body function. Inside the body, goroutines are spawned via Scope.Go. Run does not return until every spawned goroutine has finished.
Errors and panics from any spawned goroutine are propagated back to the caller of Run. The first non-nil error cancels the scope's context, which is passed to every spawned goroutine and should be observed by callers for cooperative cancellation.
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Run ¶
Run executes body inside a new Scope derived from parent and waits for all goroutines spawned via Scope.Go to finish before returning.
Run guarantees that every goroutine spawned through the Scope has returned by the time Run returns, regardless of whether body returns normally, returns an error, or panics.
If body returns a non-nil error, the scope's context is canceled and the error is returned. If any spawned goroutine returns a non-nil error or panics, the scope's context is canceled and the first such error is returned. Panics from spawned goroutines are recovered and converted into errors; they do not crash the process.
The context passed to body and to each spawned goroutine is derived from parent and is canceled as described above. Callers should observe this context (typically via ctx.Done()) to participate in cancellation.
Example ¶
ExampleRun shows a minimal usage: spawn two goroutines and wait for both.
package main
import (
"context"
"fmt"
"github.com/kakkky/scope"
)
func main() {
ctx := context.Background()
err := scope.Run(ctx, func(s *scope.Scope) error {
s.Go(func(ctx context.Context) error {
fmt.Println("task A")
return nil
})
s.Go(func(ctx context.Context) error {
fmt.Println("task B")
return nil
})
return nil
})
if err != nil {
fmt.Println("err:", err)
}
}
Output: task A task B
Example (ErrorPropagation) ¶
ExampleRun_errorPropagation shows how the first non-nil error cancels the scope and is returned to the caller.
package main
import (
"context"
"errors"
"fmt"
"github.com/kakkky/scope"
)
func main() {
ctx := context.Background()
err := scope.Run(ctx, func(s *scope.Scope) error {
s.Go(func(ctx context.Context) error {
return errors.New("task A failed")
})
s.Go(func(ctx context.Context) error {
<-ctx.Done()
return ctx.Err()
})
return nil
})
fmt.Println("err:", err)
}
Output: err: task A failed
Example (PanicRecovery) ¶
ExampleRun_panicRecovery shows that a panic inside a spawned goroutine is recovered and surfaced as an error rather than crashing the process.
package main
import (
"context"
"fmt"
"github.com/kakkky/scope"
)
func main() {
ctx := context.Background()
err := scope.Run(ctx, func(s *scope.Scope) error {
s.Go(func(ctx context.Context) error {
panic("something bad")
})
return nil
})
// err contains "scope: panic recovered: something bad" followed by a stack trace.
fmt.Println(err != nil)
}
Output: true
Types ¶
type Scope ¶
type Scope struct {
// contains filtered or unexported fields
}
Scope represents a structured concurrency scope.
A Scope owns a derived context.Context that is canceled when any spawned goroutine returns a non-nil error, when a panic is recovered, when the body function passed to Run returns a non-nil error, or when Run returns.
A Scope must be obtained via Run; callers should not construct one directly. The zero value of Scope is not usable.
func (*Scope) Go ¶
Go starts fn in a new goroutine bound to the scope.
The goroutine receives the scope's context as its argument; fn is expected to observe ctx.Done() or to pass ctx to downstream context-aware operations in order to participate in cancellation.
If fn returns a non-nil error, the scope's context is canceled and the error becomes the result of Run (unless an earlier error has already been recorded). If fn panics, the panic is recovered, formatted as an error with a stack trace, and propagated through the same mechanism.
Go may be called from inside body, from inside another goroutine spawned by the same scope, or recursively, as long as Run has not yet returned.
Example (DynamicSpawn) ¶
ExampleScope_Go_dynamicSpawn shows that goroutines can be spawned dynamically from within other goroutines, as long as Run has not yet returned.
package main
import (
"context"
"fmt"
"sync/atomic"
"github.com/kakkky/scope"
)
func main() {
ctx := context.Background()
var count atomic.Int64
err := scope.Run(ctx, func(s *scope.Scope) error {
s.Go(func(ctx context.Context) error {
for i := 0; i < 3; i++ {
s.Go(func(ctx context.Context) error {
count.Add(1)
return nil
})
}
return nil
})
return nil
})
fmt.Println("err:", err)
fmt.Println("count:", count.Load())
}
Output: err: <nil> count: 3