Help us shape the future of Go! Take the 2026 Go Developer Survey

scope

package module
v0.9.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 8, 2026 License: MIT Imports: 7 Imported by: 0

README

scope

CI Go Reference Go Report Card codecov

Scope-bound structured concurrency for Go.

scope binds the lifetime of every goroutine you spawn to a lexical block. When the block exits, every goroutine has either returned or been cancelled — no leaks, no panics that crash the process.

Install

go get github.com/kakkky/scope

Requires Go 1.27 or later. If you are on an older Go version, use v0.8.0.

Quick Start

err := scope.Run(context.Background(), func(s *scope.Scope) error {
    s.Go(func(ctx context.Context) error {
        return doA(ctx)
    })
    s.Go(func(ctx context.Context) error {
        return doB(ctx)
    })
    return nil
})
// By the time we get here, both goroutines have finished.

scope.Run does not return until every goroutine spawned via s.Go has finished. When the first non-nil error occurs, the scope's context is cancelled so sibling goroutines can observe ctx.Done() and exit. Panics are recovered into errors and returned through the same path.

API Reference

For full documentation, see pkg.go.dev/github.com/kakkky/scope.

Run
func Run(ctx context.Context, body func(s *Scope) error, opts ...Option) error

Establishes a scope, executes body, and waits for all goroutines spawned via s.Go to finish before returning. The scope's context is derived from ctx and cancelled when any goroutine or body returns a non-nil error, or when a panic is recovered.

Scope.Go
func (s *Scope) Go(fn func(ctx context.Context) error)

Spawns fn in a new goroutine bound to the scope. fn receives the scope's context so it can participate in cancellation. Go may be called from within body, from another goroutine spawned by the same scope, or recursively — all goroutines remain bound to the enclosing Run.

Scope.GoFuture[T]
func (s *Scope) GoFuture[T any](fn func(ctx context.Context) (T, error)) Future[T]

Spawns fn in a new goroutine and returns a Future[T] to retrieve the result. future.Wait() blocks until the value is available or the scope's context is cancelled.

Scope.Scope
func (s *Scope) Scope(body func(child *Scope) error, opts ...Option)

Creates a child scope nested under s and runs body synchronously within it. The call blocks until body and all goroutines spawned in the child have finished. Cancellation propagates from parent to child; errors in the child propagate back and cancel the parent's context.

Options

Options are passed to Run or Scope.Scope to configure the scope. Each scope opts in independently; options are not inherited by child scopes.

WithSupervisor
func WithSupervisor() Option

In supervisor mode, a failure in one goroutine does not cancel the context of sibling goroutines. Each goroutine runs to completion regardless of others' results.

WithErrAggregation
func WithErrAggregation() Option

By default, only the first error is returned (first-error-wins). With this option, all errors from goroutines and body are collected and returned as a single error via errors.Join. Most useful in combination with WithSupervisor.

WithMaxConcurrency
func WithMaxConcurrency(max int) Option

Limits the number of goroutines that may execute concurrently within the scope. When the limit is reached, calls to Go block until a running goroutine completes or the scope's context is cancelled.

WithTimeout
func WithTimeout(d time.Duration) Option

Cancels the scope's context after duration d. When the timeout fires, Run returns context.DeadlineExceeded. If an earlier deadline is already set on the parent context, that deadline takes precedence.

WithCancelOnSuccess
func WithCancelOnSuccess() Option

Cancels the scope's context as soon as any goroutine returns a nil error, allowing sibling goroutines to observe ctx.Done() and exit early. Run returns nil when this option triggers the cancellation.

Background

This package is inspired by the structured concurrency concept:

License

MIT — see LICENSE.

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.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func Run

func Run(ctx context.Context, body func(s *Scope) error, opts ...Option) error

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 (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

func (f Future[T]) Wait() (T, error)

Wait blocks until the goroutine produces a value and returns it, or until ctx is canceled, in which case it returns the zero value of T and ctx.Err().

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.

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.

Run returns nil when this option triggers the cancellation, even if other goroutines subsequently return ctx.Err().

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

func WithMaxConcurrency(max int) Option

WithMaxConcurrency returns an Option that limits the number of goroutines that may execute concurrently within the scope.

When the limit is reached, calls to Go block until a running goroutine completes or the scope's context is canceled.

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 context of sibling goroutines or the parent scope.

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

func WithTimeout(d time.Duration) Option

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.

If an earlier deadline is already set on the context, that deadline takes precedence and this timeout has no additional effect.

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

func (s *Scope) Go(fn func(ctx context.Context) error)

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

func (s *Scope) GoFuture[T any](fn func(ctx context.Context) (T, error)) Future[T]

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, the error is propagated through the scope's error handling mechanism (same as Go) and Wait will unblock via ctx cancellation.

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

func (*Scope) Scope added in v0.2.0

func (s *Scope) Scope(body func(child *Scope) error, opts ...Option)

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)

In supervisor mode, goroutine failures within a child scope do not cancel sibling goroutines inside that scope, nor goroutines in the parent scope.

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: false
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

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL