Documentation
¶
Overview ¶
Package scope provides scope-bound structured concurrency primitives for Go.
Structured concurrency brings the essence of structured programming to concurrent code: child tasks must not outlive the scope of their parent. This constraint prevents goroutine leaks, which are among the most common concurrency bugs in Go.
While sync.WaitGroup and golang.org/x/sync/errgroup can achieve similar guarantees via Wait(), scope takes a different approach by binding goroutine lifetimes to lexical scopes. Because the scope boundary is visible in the code structure itself, goroutine lifetimes become easier to reason about at a glance.
The primary entry point is Run. Run establishes a scope and invokes a body function. Inside the body, Scope.Go spawns goroutines. Run does not return until all spawned goroutines have completed. Child scopes can be created with Scope.Scope.
Errors and panics from spawned goroutines propagate to the caller of Run. The first non-nil error cancels the scope's context, which is passed to every goroutine and should be monitored for cooperative cancellation.
Errors and panics ¶
By default, Run returns only the first error (first-error-wins); with WithErrAggregation, it returns all errors joined via errors.Join. A panic in a goroutine spawned via Go or GoFuture is recovered into an error and handled like any other error, but it takes precedence over ordinary errors when Run picks the error to return, and WithCancelOnSuccess never discards it. A panic in the body passed to Run or Scope.Scope is not recovered: the scope is canceled, its goroutines are waited for, and the panic propagates to the caller.
When the scope's own WithTimeout fires, Run returns context.DeadlineExceeded. A deadline of the context passed to Run is not treated as the scope's own timeout; its error is returned only if something in the scope returns it.
Options and child scopes ¶
Options passed to Run or Scope.Scope apply only to the scope they are passed to. They are not inherited by child scopes, and the options of a child scope never change how its parent behaves.
From the parent's point of view, a child scope created via Scope.Scope is handled like a single goroutine spawned via Scope.Go (except that a panic in the child's body is not recovered and propagates to the parent's body), and the parent reacts to the child's result according to the parent's own options: WithSupervisor decides whether a failed child cancels the parent's context, WithErrAggregation decides whether the child's error is collected together with other errors, and WithCancelOnSuccess treats a child that returns without error as a success. The options of the child only determine what the child returns, such as a joined error under WithErrAggregation or context.DeadlineExceeded under WithTimeout.
Cancellation flows from parent to child only: canceling a scope cancels all of its descendants through the context, while a child affects its parent only through the result it returns. In particular, WithTimeout on the parent bounds all of its descendants; WithTimeout on a child can only shorten the deadline within that bound.
The limit set by WithMaxConcurrency counts only goroutines spawned via Go on that scope. A child scope runs synchronously in the caller's goroutine and does not occupy a slot of its parent's limit.
Index ¶
Examples ¶
- Run
- Run (ContextCause)
- Run (ErrorCancelsContext)
- Run (PanicRecovery)
- Run (WithCancelOnSuccess)
- Run (WithErrAggregation)
- Run (WithMaxConcurrency)
- Run (WithSupervisor)
- Run (WithSupervisorAndErrAggregation)
- Run (WithTimeout)
- Scope.Go (DynamicSpawn)
- Scope.Go (ErrorOccuredFromInnerGoRunWithSupervisor)
- Scope.GoFuture (Dag)
- Scope.GoFuture (WaitAfterRun)
- Scope.Scope
- Scope.Scope (ErrAggregationChildOnly)
- Scope.Scope (ErrAggregationParentAndChild)
- Scope.Scope (ErrAggregationParentOnly)
- Scope.Scope (ErrorCancelsContext)
- Scope.Scope (ErrorOccuredFromGoWithSupervisor)
- Scope.Scope (WithMaxConcurrency)
- Scope.Scope (WithTimeout)
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. A panic in body is not recovered: the scope's context is canceled, Run waits for all spawned goroutines, and then the panic continues to propagate to the caller.
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. A recovered panic takes precedence over ordinary errors: if any goroutine panicked, Run returns the first panic instead of the first error.
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 (ContextCause) ¶
ExampleRun_contextCause shows that context.Cause returns the error that triggered cancellation, rather than the generic context.Canceled.
package main
import (
"context"
"errors"
"fmt"
"github.com/kakkky/scope"
)
func main() {
scope.Run(context.Background(), 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()
fmt.Println("cause:", context.Cause(ctx))
return ctx.Err()
})
return nil
})
}
Output: cause: task A failed
Example (ErrorCancelsContext) ¶
ExampleRun_errorCancelsContext shows how the first non-nil error cancels the scope's context so sibling goroutines can observe ctx.Done() and exit.
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()
fmt.Println("task B: context cancelled")
return ctx.Err()
})
return nil
})
fmt.Println("err:", err)
}
Output: task B: context cancelled 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"
"strings"
"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
})
fmt.Println(strings.Contains(err.Error(), "scope: panic recovered: something bad"))
}
Output: true
Example (WithCancelOnSuccess) ¶
ExampleRun_withCancelOnSuccess shows a hedged request pattern: send the same request to multiple backends and use the first successful response, canceling the rest.
package main
import (
"context"
"fmt"
"sync"
"github.com/kakkky/scope"
)
func main() {
ctx := context.Background()
var (
once sync.Once
result string
)
err := scope.Run(ctx, func(s *scope.Scope) error {
s.Go(func(ctx context.Context) error {
// fast backend: responds immediately
once.Do(func() { result = "response from A" })
return nil
})
s.Go(func(ctx context.Context) error {
// slow backend: canceled before responding
<-ctx.Done()
once.Do(func() { result = "response from B" })
return ctx.Err()
})
return nil
}, scope.WithCancelOnSuccess())
fmt.Println(result)
fmt.Println("err:", err)
}
Output: response from A err: <nil>
Example (WithErrAggregation) ¶
ExampleRun_withErrAggregation shows that all goroutine errors are collected when WithErrAggregation is enabled. Without it, only the first error is returned.
package main
import (
"context"
"errors"
"fmt"
"github.com/kakkky/scope"
)
func main() {
errA := errors.New("task A failed")
errB := errors.New("task B failed")
err := scope.Run(context.Background(), func(s *scope.Scope) error {
s.Go(func(ctx context.Context) error { return errA })
s.Go(func(ctx context.Context) error { return errB })
return nil
}, scope.WithErrAggregation())
fmt.Println(errors.Is(err, errA))
fmt.Println(errors.Is(err, errB))
}
Output: true true
Example (WithMaxConcurrency) ¶
ExampleRun_withMaxConcurrency shows that WithMaxConcurrency limits the number of goroutines executing concurrently within a scope. At most 3 goroutines run at the same time regardless of how many are spawned.
package main
import (
"context"
"fmt"
"sync/atomic"
"github.com/kakkky/scope"
)
func main() {
var count atomic.Int64
scope.Run(context.Background(), func(s *scope.Scope) error {
for range 10 {
s.Go(func(ctx context.Context) error {
count.Add(1)
return nil
})
}
return nil
}, scope.WithMaxConcurrency(3))
fmt.Println("all tasks completed:", count.Load() == 10)
}
Output: all tasks completed: true
Example (WithSupervisor) ¶
package main
import (
"context"
"errors"
"fmt"
"github.com/kakkky/scope"
)
func main() {
ctx := context.Background()
ch := make(chan struct{})
scope.Run(ctx, func(s *scope.Scope) error {
s.Go(func(ctx context.Context) error {
close(ch)
return errors.New("task failed")
})
s.Go(func(ctx context.Context) error {
<-ch
fmt.Println("ctx cancelled:", ctx.Err() != nil)
return nil
})
return nil
}, scope.WithSupervisor())
}
Output: ctx cancelled: false
Example (WithSupervisorAndErrAggregation) ¶
ExampleRun_withSupervisorAndErrAggregation shows that combining WithSupervisor and WithErrAggregation allows collecting all errors from goroutines that run to completion without being cancelled by sibling failures.
package main
import (
"context"
"errors"
"fmt"
"github.com/kakkky/scope"
)
func main() {
errA := errors.New("task A failed")
errB := errors.New("task B failed")
err := scope.Run(context.Background(), func(s *scope.Scope) error {
s.Go(func(ctx context.Context) error { return errA })
s.Go(func(ctx context.Context) error { return errB })
return nil
}, scope.WithSupervisor(), scope.WithErrAggregation())
fmt.Println(errors.Is(err, errA))
fmt.Println(errors.Is(err, errB))
}
Output: true true
Example (WithTimeout) ¶
ExampleRun_withTimeout shows that WithTimeout cancels the scope's context after the given duration, causing all goroutines to observe ctx.Done().
package main
import (
"context"
"errors"
"fmt"
"time"
"github.com/kakkky/scope"
)
func main() {
err := scope.Run(context.Background(), func(s *scope.Scope) error {
s.Go(func(ctx context.Context) error {
<-ctx.Done()
return ctx.Err()
})
return nil
}, scope.WithTimeout(10*time.Millisecond))
fmt.Println(errors.Is(err, context.DeadlineExceeded))
}
Output: true
Types ¶
type Future ¶ added in v0.7.0
type Future[T any] struct { // contains filtered or unexported fields }
Future represents a value that will be produced by a goroutine started with Scope.GoFuture. The zero value is not usable; obtain one via Scope.GoFuture.
func (Future[T]) Wait ¶ added in v0.7.0
Wait blocks until the goroutine started by Scope.GoFuture has finished and returns the value and error returned by fn.
If fn panics, Wait returns the zero value of T and an error describing the panic. If fn was never started because the scope's context was canceled while waiting for a concurrency slot (see WithMaxConcurrency), Wait returns the zero value of T and the cancellation cause of the scope.
Wait may be called any number of times, from any goroutine, and also after Run has returned; every call returns the same result.
type Option ¶ added in v0.2.0
type Option func(*options)
Option configures a Scope created by Run or Scope.Scope.
func WithCancelOnSuccess ¶ added in v0.8.0
func WithCancelOnSuccess() Option
WithCancelOnSuccess returns an Option that cancels the scope's context as soon as any spawned goroutine returns a nil error.
A child scope created via Scope.Scope counts as a success when it returns without error, in the same way as a goroutine spawned via Go.
By default, the scope's context is only canceled when a goroutine returns a non-nil error. With this option, a successful return also triggers cancellation, allowing sibling goroutines to observe ctx.Done() and exit early.
Errors returned by goroutines or child scopes after this option has triggered the cancellation are discarded, since they are typically ctx.Err(). Errors recorded before the cancellation and errors returned by the body function are still returned by Run, and panics are never discarded by this option.
This option is not inherited by child scopes created via Scope.Scope; each scope must opt in independently. Cancellation, however, propagates to child scopes as with any context cancellation.
func WithErrAggregation ¶ added in v0.4.0
func WithErrAggregation() Option
WithErrAggregation returns an Option that enables error aggregation for the scope.
By default, only the first non-nil error is recorded (first-error-wins). With this option, all errors from goroutines and the body are collected and returned as a single error via errors.Join when Run returns.
This option is not inherited by child scopes created via Scope.Scope; each scope must opt in independently.
WithErrAggregation is most useful in combination with WithSupervisor, where goroutines continue running after a sibling fails and multiple errors can accumulate.
func WithMaxConcurrency ¶ added in v0.5.0
WithMaxConcurrency returns an Option that limits the number of goroutines that may execute concurrently within the scope.
Go never blocks: when the limit is reached, the spawned goroutine waits for a running goroutine to complete before running fn. If the scope's context is canceled while waiting, fn is never run. The limit applies to fn executions, not to goroutines waiting for a slot.
This option is not inherited by child scopes created via Scope.Scope; each scope must opt in independently.
func WithSupervisor ¶ added in v0.3.0
func WithSupervisor() Option
WithSupervisor returns an Option that enables supervisor mode for the scope.
In supervisor mode, a failure in one goroutine or child scope does not cancel the scope's context, so sibling goroutines and child scopes keep running.
Whether a failed child scope created via Scope.Scope cancels its parent is determined by the parent's mode only. WithSupervisor passed to the child affects only goroutines within the child; a failure in it still cancels a parent that is not in supervisor mode.
Note that a non-nil error returned directly from the body function still cancels the scope's context regardless of this option.
Goroutines spawned via Go from within another goroutine are registered on the same scope and treated as siblings in terms of context cancellation.
This option is not inherited by child scopes created via Scope.Scope; each scope must opt in independently.
func WithTimeout ¶ added in v0.6.0
WithTimeout returns an Option that cancels the scope's context after duration d, causing all goroutines in the scope to observe ctx.Done().
When the timeout fires, Run returns context.DeadlineExceeded. The deadline is visible via ctx.Deadline() within the scope's goroutines, allowing downstream context-aware operations (e.g. HTTP clients) to observe the remaining time.
The cause of the scope's context, as reported by context.Cause, is an error wrapping context.DeadlineExceeded.
If an earlier deadline is already set on the context, that deadline takes precedence and this timeout has no additional effect: Run returns the errors recorded in the scope, as if this option were not given.
A timeout always cancels the scope's context regardless of WithSupervisor.
The timeout setting itself is not inherited by child scopes created via Scope.Scope; each scope must opt in independently. Cancellation, however, propagates to child scopes as with any context cancellation.
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 range 3 {
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
Example (ErrorOccuredFromInnerGoRunWithSupervisor) ¶
In supervisor mode, a goroutine spawned from within another goroutine is treated as a sibling at the scope level in terms of context cancellation. Even if the spawning goroutine fails, neither the inner goroutine nor sibling goroutines have their context cancelled.
package main
import (
"context"
"errors"
"fmt"
"github.com/kakkky/scope"
)
func main() {
ctx := context.Background()
ch1 := make(chan struct{})
ch2 := make(chan struct{})
scope.Run(ctx, func(s *scope.Scope) error {
s.Go(func(ctx context.Context) error {
s.Go(func(ctx context.Context) error {
<-ch1
fmt.Println("inner ctx cancelled:", ctx.Err() != nil)
close(ch2)
return nil
})
close(ch1)
return errors.New("task failed")
})
s.Go(func(ctx context.Context) error {
<-ch2
fmt.Println("sibling ctx cancelled:", ctx.Err() != nil)
return nil
})
return nil
}, scope.WithSupervisor())
}
Output: inner ctx cancelled: false sibling ctx cancelled: false
func (*Scope) GoFuture ¶ added in v0.9.0
GoFuture starts fn in a new goroutine bound to the scope and returns a Future that can be used to retrieve the result via Future.Wait.
If fn returns a non-nil error or panics, the error is propagated through the scope's error handling mechanism (same as Go) and is also returned by Wait.
Example (Dag) ¶
ExampleScope_GoFuture_dag shows how Scope.GoFuture enables DAG-style computation. A and B are computed in parallel; C depends on A and waits inside its goroutine. The final goroutine combines B and C once both are ready.
package main
import (
"context"
"fmt"
"github.com/kakkky/scope"
)
func main() {
scope.Run(context.Background(), func(s *scope.Scope) error {
aF := s.GoFuture(func(ctx context.Context) (int, error) {
return 3 * 3, nil // A = 9
})
bF := s.GoFuture(func(ctx context.Context) (int, error) {
return 2 * 2, nil // B = 4, runs in parallel with A
})
// C waits on A inside the goroutine
cF := s.GoFuture(func(ctx context.Context) (int, error) {
a, err := aF.Wait()
if err != nil {
return 0, err
}
return a + 1, nil // C = A + 1 = 10
})
// final result waits on B and C inside the goroutine
s.Go(func(ctx context.Context) error {
b, err := bF.Wait()
if err != nil {
return err
}
c, err := cF.Wait()
if err != nil {
return err
}
fmt.Println(b + c) // B + C = 14
return nil
})
return nil
})
}
Output: 14
Example (WaitAfterRun) ¶
ExampleScope_GoFuture_waitAfterRun shows how to collect results after Run returns. With WithSupervisor, a failure in one task does not cancel the others, so each Future can be inspected individually to handle partial success.
package main
import (
"context"
"errors"
"fmt"
"github.com/kakkky/scope"
)
func main() {
var userF, ordersF scope.Future[string]
err := scope.Run(context.Background(), func(s *scope.Scope) error {
userF = s.GoFuture(func(ctx context.Context) (string, error) {
return "alice", nil
})
ordersF = s.GoFuture(func(ctx context.Context) (string, error) {
return "", errors.New("orders: service unavailable")
})
return nil
}, scope.WithSupervisor(), scope.WithErrAggregation())
fmt.Println("Run:", err)
// Run has already waited for every goroutine, so Wait returns immediately.
if user, err := userF.Wait(); err == nil {
fmt.Println("user:", user)
}
if _, err := ordersF.Wait(); err != nil {
fmt.Println("orders failed:", err)
}
}
Output: Run: orders: service unavailable user: alice orders failed: orders: service unavailable
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.
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>
Example (ErrAggregationChildOnly) ¶
ExampleScope_Scope_errAggregationChildOnly shows that when only the child scope has WithErrAggregation, the child aggregates its errors into a single joined error, which the root records under first-error-wins policy. Since Scope is synchronous, the child's joined error is always recorded first; the root's Go error arrives later and is discarded.
package main
import (
"context"
"errors"
"fmt"
"github.com/kakkky/scope"
)
func main() {
errA := errors.New("task A failed")
errB := errors.New("task B failed")
errC := errors.New("task C failed")
err := scope.Run(context.Background(), func(s *scope.Scope) error {
s.Scope(func(child *scope.Scope) error {
child.Go(func(ctx context.Context) error { return errA })
child.Go(func(ctx context.Context) error { return errB })
return nil
}, scope.WithErrAggregation())
s.Go(func(ctx context.Context) error { return errC })
return nil
})
fmt.Println(errors.Is(err, errA))
fmt.Println(errors.Is(err, errB))
fmt.Println(errors.Is(err, errC))
}
Output: true true false
Example (ErrAggregationParentAndChild) ¶
ExampleScope_Scope_errAggregationParentAndChild shows that each scope must opt in to WithErrAggregation independently — it is not inherited. When both parent and child have WithErrAggregation, all errors from both levels are visible in the result.
package main
import (
"context"
"errors"
"fmt"
"github.com/kakkky/scope"
)
func main() {
errA := errors.New("task A failed")
errB := errors.New("task B failed")
errC := errors.New("task C failed")
err := scope.Run(context.Background(), func(s *scope.Scope) error {
s.Go(func(ctx context.Context) error { return errA })
s.Scope(func(child *scope.Scope) error {
child.Go(func(ctx context.Context) error { return errB })
child.Go(func(ctx context.Context) error { return errC })
return nil
}, scope.WithErrAggregation())
return nil
}, scope.WithErrAggregation())
fmt.Println(errors.Is(err, errA))
fmt.Println(errors.Is(err, errB))
fmt.Println(errors.Is(err, errC))
}
Output: true true true
Example (ErrAggregationParentOnly) ¶
ExampleScope_Scope_errAggregationParentOnly shows that when only the parent has WithErrAggregation, the child uses first-error-wins and propagates only one error to the parent. The root also has its own Go, so the root aggregates both: the single error from the child and the error from s.Go.
package main
import (
"context"
"errors"
"fmt"
"github.com/kakkky/scope"
)
func main() {
errA := errors.New("task A failed")
errB := errors.New("task B failed")
errC := errors.New("task C failed")
err := scope.Run(context.Background(), func(s *scope.Scope) error {
s.Scope(func(child *scope.Scope) error {
child.Go(func(ctx context.Context) error { return errA })
child.Go(func(ctx context.Context) error { return errB })
return nil
})
s.Go(func(ctx context.Context) error { return errC })
return nil
}, scope.WithErrAggregation())
// root aggregates: one of errA/errB (child's first-error-wins) and errC
fmt.Println((errors.Is(err, errA) || errors.Is(err, errB)) && errors.Is(err, errC))
}
Output: true
Example (ErrorCancelsContext) ¶
ExampleScope_Scope_errorCancelsContext shows that an error in a child scope cancels the parent's context so sibling goroutines can observe ctx.Done() and exit.
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 {
<-ctx.Done()
fmt.Println("sibling: context cancelled")
return ctx.Err()
})
s.Scope(func(child *scope.Scope) error {
return errors.New("child scope failed")
})
return nil
})
fmt.Println("err:", err)
}
Output: sibling: context cancelled err: child scope failed
Example (ErrorOccuredFromGoWithSupervisor) ¶
WithSupervisor on a child scope only affects goroutines within that scope: sibling goroutines inside it are not canceled, but the failure still cancels a parent that is not in supervisor mode.
package main
import (
"context"
"errors"
"fmt"
"github.com/kakkky/scope"
)
func main() {
ctx := context.Background()
ch1 := make(chan struct{})
err := scope.Run(ctx, func(s *scope.Scope) error {
s.Scope(func(child *scope.Scope) error {
child.Go(func(ctx context.Context) error {
close(ch1)
return errors.New("task failed")
})
child.Go(func(ctx context.Context) error {
<-ch1
fmt.Println("sibling ctx cancelled:", ctx.Err() != nil)
return nil
})
return nil
}, scope.WithSupervisor())
s.Go(func(ctx context.Context) error {
fmt.Println("parent level goroutine ctx cancelled:", ctx.Err() != nil)
return nil
})
return nil
})
fmt.Println("err:", err)
}
Output: sibling ctx cancelled: false parent level goroutine ctx cancelled: true err: task failed
Example (WithMaxConcurrency) ¶
ExampleScope_Scope_withMaxConcurrency shows that WithMaxConcurrency is independent per scope and is not inherited by child scopes.
package main
import (
"context"
"fmt"
"sync/atomic"
"github.com/kakkky/scope"
)
func main() {
var rootCount, childCount atomic.Int64
scope.Run(context.Background(), func(s *scope.Scope) error {
for range 5 {
s.Go(func(ctx context.Context) error {
rootCount.Add(1)
return nil
})
}
s.Scope(func(child *scope.Scope) error {
for range 5 {
child.Go(func(ctx context.Context) error {
childCount.Add(1)
return nil
})
}
return nil
}, scope.WithMaxConcurrency(2))
return nil
}, scope.WithMaxConcurrency(3))
fmt.Println("root tasks completed:", rootCount.Load() == 5)
fmt.Println("child tasks completed:", childCount.Load() == 5)
}
Output: root tasks completed: true child tasks completed: true
Example (WithTimeout) ¶
ExampleScope_Scope_withTimeout shows that a child scope can have its own timeout independent of the parent. When the child times out, the error propagates to the parent scope.
package main
import (
"context"
"errors"
"fmt"
"time"
"github.com/kakkky/scope"
)
func main() {
err := scope.Run(context.Background(), func(s *scope.Scope) error {
s.Scope(func(child *scope.Scope) error {
child.Go(func(ctx context.Context) error {
<-ctx.Done()
return ctx.Err()
})
return nil
}, scope.WithTimeout(10*time.Millisecond))
return nil
})
fmt.Println(errors.Is(err, context.DeadlineExceeded))
}
Output: true