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.
The opts parameter is reserved for future use; currently no options are defined and any provided values are ignored.
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 Option ¶ added in v0.2.0
type Option func(*options)
Option configures a Scope created by Run or Scope.Scope.
No options are currently defined. The Option type exists so that future configuration (such as timeouts or error policies) can be added without breaking existing callers.
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.
Calling Go after Run has returned panics. Goroutines must be spawned while Run is executing.
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
func (*Scope) Scope ¶ added in v0.2.0
Scope creates a child scope nested under s and runs body within it.
Scope is synchronous: it does not return until body has returned and every goroutine spawned in the child scope has finished. The child scope's context is derived from s's context, so cancellation of s propagates to the child and to all goroutines spawned within it.
Errors and panics originating inside the child scope (from body itself or from any goroutine spawned via the child's Go) are propagated to s through the same mechanism as Scope.Go: the first such error becomes the result of the enclosing Run (unless an earlier error has already been recorded), and s's context is canceled so sibling goroutines spawned via s.Go can observe cancellation.
Scope makes the nested structure of a scope tree explicit but does not itself introduce concurrency.
Scope is intended to be called from the body of Run or from another Scope's body, not from a goroutine spawned via Scope.Go.
The opts parameter is reserved for future use; currently no options are defined and any provided values are ignored.
Calling Scope after Run has returned panics, matching the behavior of Scope.Go.
Example ¶
ExampleScope_Scope shows how a child scope groups work and blocks until every goroutine spawned inside it has finished.
package main
import (
"context"
"fmt"
"github.com/kakkky/scope"
)
func main() {
ctx := context.Background()
err := scope.Run(ctx, func(s *scope.Scope) error {
s.Scope(func(child *scope.Scope) error {
child.Go(func(ctx context.Context) error {
fmt.Println("inside child scope")
return nil
})
return nil
})
fmt.Println("after child scope")
return nil
})
fmt.Println("err:", err)
}
Output: inside child scope after child scope err: <nil>