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

bedrock

package module
v0.24.0 Latest Latest
Warning

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

Go to latest
Published: Oct 16, 2026 License: MIT Imports: 6 Imported by: 7

README

bedrock

Mentioned in Awesome Go Go Reference Go Report Card build

bedrock is a minimal foundation for building use case specific frameworks in Go.

It is meant for teams, such as platform engineering teams, who build their own frameworks on top of it. If you want a framework to build services with, see z5labs/humus, which is built on bedrock.

An application is an OS process

The operating system hands a process its arguments, its environment, its standard streams and the delivery of its signals, and takes back an exit status. bedrock hands an application exactly those things, as one bedrock.Process value, and takes back a bedrock.ExitStatus:

type App interface {
	Run(ctx context.Context, p bedrock.Process) bedrock.ExitStatus
}

That call is the boundary. An App never reads os.Args, os.Getenv or os.Stdout, never calls signal.Notify and never calls os.Exit. Everything it learns from its process comes through p, and the only thing it does to its process is return a status:

var greeter = bedrock.AppFunc(func(ctx context.Context, p bedrock.Process) bedrock.ExitStatus {
	greeting, err := config.Env(p.Environment, "GREETING").Default("hello").Read(ctx)
	if err != nil {
		return fail(p, err)
	}

	ctx, stop := p.NotifyContext(ctx, syscall.SIGTERM)
	defer stop()

	if _, err := fmt.Fprintln(p.Stdout, greeting); err != nil {
		return fail(p, err)
	}
	<-ctx.Done()
	return bedrock.ExitWithCause(0, context.Cause(ctx))
})

// fail reports err on the App's standard error, because bedrock.Main prints
// nothing, and returns it as the cause of the status so a test can assert on it.
func fail(p bedrock.Process, err error) bedrock.ExitStatus {
	if _, werr := fmt.Fprintln(p.Stderr, err); werr != nil {
		return bedrock.ExitWithCause(1, errors.Join(err, werr))
	}
	return bedrock.ExitWithCause(1, err)
}

main is one line. bedrock.Main runs the App on the real process and exits with the code of the status it returns:

func main() { bedrock.Main(greeter) }

Because nothing reaches around the boundary, a test runs the whole App on a fake process. It never touches the environment, the streams or the signals of the test binary, so it runs in parallel and inside a testing/synctest bubble:

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

	fp, statusCh := bedrocktest.Start(t, greeter, bedrocktest.WithEnv(map[string]string{"GREETING": "hi"}))

	<-fp.Notified(syscall.SIGTERM)
	fp.Signal(syscall.SIGTERM)
	status := <-statusCh

	require.Equal(t, 0, status.Code())
	require.Equal(t, "hi\n", fp.Stdout())
}

Packages

package what it is
bedrock App, Process, ExitStatus, Environment and Main, the one place bedrock touches the real process.
bedrocktest A hermetic fake Process for tests that run a whole App, in the way net/http/httptest serves handlers.
config Configuration values composed by chaining, read from the environment of the process, and config.Struct to read many at once.
example/httpserver The reference for building on bedrock: a complete HTTP service with the standard library alone, with configuration, graceful shutdown and whole-service tests.

Library code imports only the standard library.

fit

fit is request-scoped failure injection after Netflix's Failure Injection Testing: a request under test carries a failure scenario as OpenTelemetry baggage, and each injection point it passes through asks an Evaluator which fault to emulate. It is a nested module with its own go.mod, so the OpenTelemetry dependency stays out of bedrock itself:

go get github.com/z5labs/bedrock/fit

fit owns no wire convention. It declares no baggage key, value grammar, size limit, signature scheme or expiry; organisations supply their own codecs, a Decoder and, at the edge, an Encoder, that decide all of them. The design is discussed in discussion #578.

circuit

circuit is a circuit breaker for one dependency: the closed, open and half-open state machine, with no I/O, no background goroutine and no defaults. A call goes through it inline, in the caller's own code:

t, err := b.Allow(ctx)
if err != nil {
	return err // a *circuit.RejectedError
}
defer t.Release()

err = call(ctx)
t.Done(err)

It is a nested module with its own go.mod, and its library code imports only the standard library:

go get github.com/z5labs/bedrock/circuit

circuit ships no policy, classifier or observer. When to open, what counts as a failure and how changes of state are reported are the caller's decisions, and transports, interceptors and metrics belong to the framework built on it. The design is discussed in discussion #595.

retry

retry retries calls to a dependency inline, in the caller's own code, by ranging over the attempts of a call:

var err error
for a := range r.Attempts(ctx) {
	err = call(ctx)
	a.Done(err)
}
return err

Done classifies each result with a function the caller provides, and returns why the loop goes on or ends. A Budget, after gRPC's retry throttling, limits the retries of every call that shares it, and Exponential backs off with full jitter.

It is a nested module with its own go.mod, and its library code imports only the standard library:

go get github.com/z5labs/bedrock/retry

retry ships no classifier. What can be retried is the caller's decision, and a circuit breaker composes with it through the classifier, which returns Stop for the breaker's rejection. Transports, interceptors and metrics belong to the framework built on it. The design is discussed in discussion #606.

Releases

Every module in this repository is released with the same version, after OpenTelemetry Go: publishing a GitHub release tagged v0.22.0 releases github.com/z5labs/bedrock at v0.22.0, and the Release workflow tags every nested module with that version too, so fit/v0.22.0 is github.com/z5labs/bedrock/fit at v0.22.0. Modules are tagged whether or not they changed, so one version number names a set of modules that were built and tested together.

Migrating from v0.21.0

bedrock v0.21.0 was built around Builder, Runtime and Runner, with HTTP, REST and OpenTelemetry runtimes. MIGRATING.md says where every one of its exports went, either to the process model, to humus or to the standard library, with before and after examples. Its documentation remains available at pkg.go.dev/github.com/z5labs/bedrock@v0.21.0.

Status

The API is in active development and is not yet stable.

Documentation

Overview

Package bedrock models an application as an OS process.

The boundary

exec hands a process its arguments, its environment, its standard streams and the delivery of its signals, and takes back an exit status. bedrock hands an App the same things as one Process value, together with a root context, and takes back an ExitStatus:

Run(ctx context.Context, p Process) ExitStatus

That call is the boundary. An App never reads os.Args, os.Getenv, os.Stdin or the other standard streams, never calls signal.Notify and never calls os.Exit. Everything it learns from its process comes through p, and the only thing it does to its process is return a status. So an App runs whole inside a test, on a hand-built Process.

The one-line main

Main is the only place bedrock touches the real process. It runs an App on OSProcess with context.Background() and exits with the code of the status it returns:

func main() { bedrock.Main(app) }

Main exits with the status code and never prints the cause.

Testing

A test builds a Process from its own values, such as a StaticEnvironment and in-memory streams, runs the App with t.Context() as its root context, and asserts on the returned status and on what the App wrote. Such a test never changes the environment of the test binary, so it can run in parallel.

The cause of an ExitStatus exists so that a negative test can assert why an App failed, for example with errors.Is. Nothing reads it in production. Anything a user should see, the App writes to p.Stderr.

Parsing flags

An App parses its flags from p.Args with a flag.FlagSet of its own, using flag.ContinueOnError and p.Stderr as the FlagSet's output. flag itself prints a parse error followed by the usage to that output, and Main prints nothing, so the user sees each exactly once. The cause of the status carries the same error for tests:

fs := flag.NewFlagSet(p.Args[0], flag.ContinueOnError)
fs.SetOutput(p.Stderr) // flag reports the parse error and usage to the user
n := fs.Int("n", 1, "how many times to greet")
switch err := fs.Parse(p.Args[1:]); {
case errors.Is(err, flag.ErrHelp):
	return bedrock.Exit(0)
case err != nil:
	return bedrock.ExitWithCause(2, err) // cause is for tests; flag already told the user
}

-h and -help print the usage and exit with status 0. The flags example of App runs this idiom whole.

Beyond the boundary

bedrock owns only what exec hands a process. The network, the filesystem, the clock and randomness belong to the App or to the framework built on bedrock, which passes them in explicitly. In tests, time comes from testing/synctest, and randomness from testing/cryptotest or a seeded source.

Child processes

An Environment offers lookup only, so it cannot be listed the way os.Environ lists the real environment. An App that starts a child process with os/exec must build the child's exec.Cmd.Env from the keys it knows, reading each through Environment.Value. It must never leave Env nil, because a nil Env inherits the environment of the real process and so reaches around the boundary.

Panics

A panic is an assertion that a programming error happened. bedrock never recovers a panic, so a panic in an App crashes the process with exit status 2.

Ambient leaks

Some of the standard library and of common dependencies read the real process behind an App's back. Avoid them, or configure them from p:

  • flag.CommandLine and flag.ExitOnError read os.Args and exit the process. Parse p.Args[1:] with a FlagSet of your own instead; see Parsing flags above.
  • An exec.Cmd with a nil Env inherits the real environment; see Child processes above.
  • http.ProxyFromEnvironment, the Proxy of http.DefaultTransport, reads the proxy variables of the real environment.
  • OpenTelemetry's resource.Default reads the real environment, and its global providers are process-wide state.
  • slog.Default writes to the real standard error unless it is replaced, and replacing it is process-wide state.

Engineering rules

bedrock's engineering rules are inspired by NASA's Power of 10 and TigerBeetle's TigerStyle. Neither is a literal standard here: each rule is applied, adapted to Go or rejected, and every rule that is applied is enforced by a tool in CI rather than by review alone.

The boundary itself is applied. golangci-lint's forbidigo fails any use of os.Args, os.Stdin, os.Stdout, os.Stderr, os.Getenv, os.LookupEnv, os.Environ, os.Exit, signal.Notify, signal.NotifyContext, flag.Parse or flag.CommandLine across every module of the repository, tests and examples included, except in the OS edge, its tests, and the test that checks the fake signal delivery of bedrocktest against the real one.

From the Power of 10:

  • Rule 1, simple control flow, is adapted: bedrock uses no goto and no recursion, which review keeps true.
  • Rule 2, fixed loop bounds, is adapted: every loop ranges over a finite value, and every wait ends when a context is done rather than after a fixed count.
  • Rule 3, no allocation after initialization, is adapted. Go allocates and collects garbage, so bedrock allocates when a value is built rather than each time it is used: NewStaticEnvironment copies its map once and a successful Value only reads it.
  • Rule 4, short functions, is adapted: functions are kept short, but table-driven tests are exempt, so no tool enforces a line limit.
  • Rule 5, assertion density, is adapted: a panic asserts a programming error and is never recovered, and Exit asserts that its code is in range, but assertions are not counted per function.
  • Rule 6, smallest scope, is adapted: bedrock has no package-level state, and review keeps every other value at the smallest scope.
  • Rule 7, check every result, is applied. errcheck runs on test code as well as library code, so no error is discarded silently, and ExitStatus.Cause lets a test check why every failure path failed, not only that it failed. Checking parameters is adapted; see Preconditions below.
  • Rule 8, limited use of the preprocessor, is adapted: Go has none, and build constraints only select a platform.
  • Rule 9, no function pointers, is rejected. AppFunc and the func fields of Process are function values, and small functions that compose are how bedrock stays small.
  • Rule 10, zero warnings, is applied: go vet and golangci-lint run on every pull request, and any finding fails it.

From TigerStyle:

  • Assertions are adapted, as for rule 5: panics assert programming errors, and preconditions are documented rather than checked.
  • Putting a limit on everything is adapted: an exit code is limited to 0..255 and Exit checks it, and waits are bounded by contexts.
  • Static memory allocation is adapted, as for rule 3.
  • Zero dependencies is adapted: the library code of this module imports only the standard library, the library code of the nested fit module imports only the standard library and OpenTelemetry's baggage package, the library code of the nested circuit and retry modules imports only the standard library, and tests alone may use third-party modules. golangci-lint's depguard enforces all four.
  • Exhaustive testing is applied as statement coverage: every non-main package of every module is at 100.0%, and CI fails a pull request that leaves any of them below it.
  • Deterministic simulation testing is not claimed; see Testing claims below.

Preconditions

The preconditions of a Process are documented rather than checked at runtime: every field is required, and code that receives a Process uses any field without a nil check. The two places that build a Process, OSProcess and bedrocktest, are guarded instead by tests that use reflection to range over every field, so a field added to Process fails them until it is set.

Testing claims

bedrock claims hermetic, black-box testing with deterministic time: an App runs whole on a fake Process, the test sees only the status it returns and what it writes, and testing/synctest makes its clock deterministic. It does not claim deterministic simulation testing. The Go scheduler, the network and the filesystem are not simulated, so a run cannot be replayed from a seed.

Example (Main)

The main function of a program built on bedrock is one line. This example has no Output comment, so it is compiled but never run: running it would exit the test binary.

package main

import (
	"context"
	"fmt"
	"os"

	"github.com/z5labs/bedrock"
)

func main() {
	app := bedrock.AppFunc(func(ctx context.Context, p bedrock.Process) bedrock.ExitStatus {
		ctx, stop := p.NotifyContext(ctx, os.Interrupt)
		defer stop()

		if _, err := fmt.Fprintln(p.Stdout, "hello, world"); err != nil {
			return bedrock.ExitWithCause(1, err)
		}

		<-ctx.Done()
		return bedrock.ExitWithCause(0, context.Cause(ctx))
	})

	// In package main:
	//
	//	func main() { bedrock.Main(app) }
	bedrock.Main(app)
}

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func Main added in v0.22.0

func Main(app App)

Main runs app on the current OS process and exits the process with the code of the ExitStatus it returns. It never returns.

Main is a shim for the signature Go's own main function does not have:

func main() { bedrock.Main(app) }

It is exactly os.Exit(app.Run(context.Background(), OSProcess()).Code()). Main ignores the status's ExitStatus.Cause and prints nothing, because the cause exists for tests; an App reports anything a user should see on Process.Stderr. A panic in app is not recovered, so it crashes the process with exit status 2.

Types

type App

type App interface {
	// Run runs the App in the given [Process] and returns the status the
	// process exits with.
	//
	// ctx is the root context of the run: [Main] passes
	// context.Background() and a test passes t.Context(). An App that
	// listens for signals derives its context from ctx through
	// [Process.NotifyContext], so cancelling ctx reaches it too.
	//
	// The code of the returned ExitStatus is the exit status of the
	// process, with 0..125 recommended, and the zero ExitStatus means
	// success. Its cause is for tests and other callers of Run: [Main]
	// ignores it, so an App reports anything a user should see on
	// [Process.Stderr].
	Run(ctx context.Context, p Process) ExitStatus
}

App is an application that runs as an OS process.

Example

An App runs whole inside a test. bedrocktest hands it a hermetic Process, so the test sets its arguments and environment, reads what it wrote, and asserts on the exit code it returned. Nothing of the real process is read or changed.

package main

import (
	"context"
	"errors"
	"fmt"
	"testing"

	"github.com/z5labs/bedrock"
	"github.com/z5labs/bedrock/bedrocktest"
)

// exampleTB stands in for the *testing.T an Example does not have. In a
// test, pass t to bedrocktest.NewProcess or bedrocktest.Start instead.
type exampleTB struct {
	testing.TB
	ctx      context.Context
	cancel   context.CancelFunc
	cleanups []func()
}

// Context returns a context that runCleanups cancels before it runs the
// cleanups, as testing does with t.Context.
func (tb *exampleTB) Context() context.Context {
	if tb.ctx == nil {
		tb.ctx, tb.cancel = context.WithCancel(context.Background())
	}
	return tb.ctx
}

func (*exampleTB) Helper() {}

func (*exampleTB) Errorf(format string, args ...any) {
	fmt.Printf("test failed: "+format+"\n", args...)
}

func (tb *exampleTB) Cleanup(f func()) {
	tb.cleanups = append(tb.cleanups, f)
}

func (tb *exampleTB) runCleanups() {
	if tb.cancel != nil {
		tb.cancel()
	}
	for i := len(tb.cleanups) - 1; i >= 0; i-- {
		tb.cleanups[i]()
	}
}

func main() {
	app := bedrock.AppFunc(func(ctx context.Context, p bedrock.Process) bedrock.ExitStatus {
		name, err := p.Environment.Value("NAME")
		if err != nil {
			if _, werr := fmt.Fprintln(p.Stderr, err); werr != nil {
				return bedrock.ExitWithCause(1, errors.Join(err, werr))
			}
			return bedrock.ExitWithCause(1, err)
		}
		if _, err := fmt.Fprintf(p.Stdout, "hello, %s\n", name); err != nil {
			return bedrock.ExitWithCause(1, err)
		}
		return bedrock.Exit(0)
	})

	t := new(exampleTB)
	defer t.runCleanups()

	fp := bedrocktest.NewProcess(t, bedrocktest.WithEnv(map[string]string{"NAME": "gopher"}))
	status := app.Run(context.Background(), fp.Process())

	fmt.Print(fp.Stdout())
	fmt.Println("exit code:", status.Code())
}
Output:
hello, gopher
exit code: 0
Example (Flags)

An App parses its flags from p.Args with flag.ContinueOnError and p.Stderr as the FlagSet's output. flag then prints a parse error and the usage to the user exactly once, Main prints nothing more, and the cause carries the same error for tests. -h is a successful exit.

package main

import (
	"context"
	"errors"
	"flag"
	"fmt"
	"strings"
	"testing"

	"github.com/z5labs/bedrock"
	"github.com/z5labs/bedrock/bedrocktest"
)

// exampleTB stands in for the *testing.T an Example does not have. In a
// test, pass t to bedrocktest.NewProcess or bedrocktest.Start instead.
type exampleTB struct {
	testing.TB
	ctx      context.Context
	cancel   context.CancelFunc
	cleanups []func()
}

// Context returns a context that runCleanups cancels before it runs the
// cleanups, as testing does with t.Context.
func (tb *exampleTB) Context() context.Context {
	if tb.ctx == nil {
		tb.ctx, tb.cancel = context.WithCancel(context.Background())
	}
	return tb.ctx
}

func (*exampleTB) Helper() {}

func (*exampleTB) Errorf(format string, args ...any) {
	fmt.Printf("test failed: "+format+"\n", args...)
}

func (tb *exampleTB) Cleanup(f func()) {
	tb.cleanups = append(tb.cleanups, f)
}

func (tb *exampleTB) runCleanups() {
	if tb.cancel != nil {
		tb.cancel()
	}
	for i := len(tb.cleanups) - 1; i >= 0; i-- {
		tb.cleanups[i]()
	}
}

// greet parses its flags with a FlagSet of its own, from p.Args, and
// reports parse errors through flag itself.
func greet(ctx context.Context, p bedrock.Process) bedrock.ExitStatus {
	fs := flag.NewFlagSet(p.Args[0], flag.ContinueOnError)
	fs.SetOutput(p.Stderr)
	n := fs.Int("n", 1, "how many times to greet")
	switch err := fs.Parse(p.Args[1:]); {
	case errors.Is(err, flag.ErrHelp):
		return bedrock.Exit(0)
	case err != nil:
		return bedrock.ExitWithCause(2, err)
	}

	for range *n {
		if _, err := fmt.Fprintln(p.Stdout, "hello"); err != nil {
			return bedrock.ExitWithCause(1, err)
		}
	}
	return bedrock.Exit(0)
}

func main() {
	app := bedrock.AppFunc(greet)
	usage := "Usage of " + bedrocktest.ProgramName + ":"

	for _, args := range [][]string{{"-n=2"}, {"-h"}, {"-bogus"}, {"-n=abc"}} {
		t := new(exampleTB)
		fp := bedrocktest.NewProcess(t, bedrocktest.WithArgs(args...))
		status := app.Run(context.Background(), fp.Process())
		t.runCleanups()

		stderr := fp.Stderr()
		fmt.Printf("%s: exit code %d\n", args[0], status.Code())
		fmt.Printf("  stdout: %q\n", fp.Stdout())
		fmt.Printf("  usage printed %d time(s)\n", strings.Count(stderr, usage))
		fmt.Printf("  cause: %v\n", status.Cause())
		if cause := status.Cause(); cause != nil {
			fmt.Printf("  cause printed %d time(s)\n", strings.Count(stderr, cause.Error()))
		}
	}
}
Output:
-n=2: exit code 0
  stdout: "hello\nhello\n"
  usage printed 0 time(s)
  cause: <nil>
-h: exit code 0
  stdout: ""
  usage printed 1 time(s)
  cause: <nil>
-bogus: exit code 2
  stdout: ""
  usage printed 1 time(s)
  cause: flag provided but not defined: -bogus
  cause printed 1 time(s)
-n=abc: exit code 2
  stdout: ""
  usage printed 1 time(s)
  cause: invalid value "abc" for flag -n: parse error
  cause printed 1 time(s)
Example (GracefulShutdown)

An App shuts down gracefully by deriving its context from Process.NotifyContext and returning once a signal cancels it. Returning ExitWithCause(0, context.Cause(ctx)) exits successfully while still telling a test which signal stopped the App.

package main

import (
	"context"
	"errors"
	"fmt"
	"syscall"
	"testing"

	"github.com/z5labs/bedrock"
	"github.com/z5labs/bedrock/bedrocktest"
)

// exampleTB stands in for the *testing.T an Example does not have. In a
// test, pass t to bedrocktest.NewProcess or bedrocktest.Start instead.
type exampleTB struct {
	testing.TB
	ctx      context.Context
	cancel   context.CancelFunc
	cleanups []func()
}

// Context returns a context that runCleanups cancels before it runs the
// cleanups, as testing does with t.Context.
func (tb *exampleTB) Context() context.Context {
	if tb.ctx == nil {
		tb.ctx, tb.cancel = context.WithCancel(context.Background())
	}
	return tb.ctx
}

func (*exampleTB) Helper() {}

func (*exampleTB) Errorf(format string, args ...any) {
	fmt.Printf("test failed: "+format+"\n", args...)
}

func (tb *exampleTB) Cleanup(f func()) {
	tb.cleanups = append(tb.cleanups, f)
}

func (tb *exampleTB) runCleanups() {
	if tb.cancel != nil {
		tb.cancel()
	}
	for i := len(tb.cleanups) - 1; i >= 0; i-- {
		tb.cleanups[i]()
	}
}

func main() {
	app := bedrock.AppFunc(func(ctx context.Context, p bedrock.Process) bedrock.ExitStatus {
		ctx, stop := p.NotifyContext(ctx, syscall.SIGTERM)
		defer stop()

		if _, err := fmt.Fprintln(p.Stdout, "serving"); err != nil {
			return bedrock.ExitWithCause(1, err)
		}

		<-ctx.Done()
		if _, err := fmt.Fprintln(p.Stdout, "shutting down"); err != nil {
			return bedrock.ExitWithCause(1, err)
		}
		return bedrock.ExitWithCause(0, context.Cause(ctx))
	})

	t := new(exampleTB)
	defer t.runCleanups()

	fp, statusCh := bedrocktest.Start(t, app)

	// Wait until the App listens for SIGTERM, then send a fake one.
	<-fp.Notified(syscall.SIGTERM)
	fp.Signal(syscall.SIGTERM)
	status := <-statusCh

	fmt.Print(fp.Stdout())
	fmt.Println("exit code:", status.Code())
	fmt.Println("cause:", status.Cause())
	fmt.Println("cancelled:", errors.Is(status.Cause(), context.Canceled))
}
Output:
serving
shutting down
exit code: 0
cause: terminated signal received
cancelled: true

type AppFunc added in v0.22.0

type AppFunc func(ctx context.Context, p Process) ExitStatus

AppFunc is a function that implements App.

func (AppFunc) Run added in v0.22.0

func (f AppFunc) Run(ctx context.Context, p Process) ExitStatus

Run implements the App interface by calling f(ctx, p).

type Environment added in v0.22.0

type Environment interface {
	// Value returns the value of the named variable, or a
	// [ValueNotFoundError] naming it when it is not set.
	Value(name string) (string, error)
}

Environment provides the environment variables of a process.

It is the only environment type in bedrock: OSEnvironment reads the environment of the current OS process and StaticEnvironment holds a fixed set of values for tests. An Environment offers lookup only, so it cannot be listed; see the package documentation for what that means for child processes.

type ExitStatus added in v0.22.0

type ExitStatus struct {
	// contains filtered or unexported fields
}

ExitStatus is the status an App exits with.

Its code is the exit status of the OS process, so it must lie in 0..255. The codes 0..125 are recommended, since a shell reports the higher ones for its own failures and for processes killed by a signal. The zero ExitStatus means success: its code is 0 and it has no cause.

A non-zero ExitStatus can only be built with Exit or ExitWithCause, both of which panic on a code outside 0..255.

func Exit added in v0.22.0

func Exit(code int) ExitStatus

Exit returns an ExitStatus with the given code and no cause.

The code is the exit status of the OS process. It must lie in 0..255, and 0..125 is recommended. Exit panics on a code outside 0..255, naming the code, so an invalid code is caught at the return statement that produced it instead of being silently truncated to 8 bits by the OS.

func ExitWithCause added in v0.22.0

func ExitWithCause(code int, cause error) ExitStatus

ExitWithCause returns an ExitStatus with the given code and the error that caused the App to exit.

The code has the same range as for Exit, and ExitWithCause panics on a code outside 0..255. The panic value is an error naming the code that wraps cause when it is non-nil.

Any code may carry a cause, including 0: a graceful shutdown can return ExitWithCause(0, context.Cause(ctx)) so that a test can assert which signal stopped the App. The cause is for tests and other callers of App.Run; Main never reads it.

func (ExitStatus) Cause added in v0.22.0

func (s ExitStatus) Cause() error

Cause returns the error that caused the App to exit, or nil if there is none.

The cause exists so that tests and other callers of App.Run can assert why an App exited, for example with errors.Is. Main ignores it and prints nothing, so an App reports anything a user should see on its own stderr.

func (ExitStatus) Code added in v0.22.0

func (s ExitStatus) Code() int

Code returns the exit code, which is the exit status of the OS process. It is always in 0..255.

type OSEnvironment added in v0.22.0

type OSEnvironment struct{}

OSEnvironment is an Environment backed by the environment of the current OS process. It is the Environment of OSProcess.

func (OSEnvironment) Value added in v0.22.0

func (OSEnvironment) Value(name string) (string, error)

Value implements the Environment interface.

type Process added in v0.22.0

type Process struct {
	// Args holds the command-line arguments, starting with the program
	// name, like os.Args. It is required.
	Args []string

	// Stdin is the standard input of the process. It is required.
	Stdin io.Reader

	// Stdout is the standard output of the process. It is required, and
	// it must be safe for concurrent writes if the App writes to it from
	// several goroutines, as an *os.File is.
	Stdout io.Writer

	// Stderr is the standard error of the process. It is required, and it
	// must be safe for concurrent writes if the App writes to it from
	// several goroutines, as an *os.File is.
	Stderr io.Writer

	// Environment provides the environment variables of the process. It
	// is required: OSEnvironment{} in production and a
	// [StaticEnvironment] in tests.
	Environment Environment

	// NotifyContext delivers signals to the process, with the semantics of
	// signal.NotifyContext, which is what [OSProcess] sets it to. It is
	// required.
	//
	// It returns a copy of parent that is done when one of signals
	// arrives, when stop is called, or when parent is done, whichever
	// happens first. Listing no signals means every signal. When a signal
	// cancels ctx, context.Cause(ctx) reports an error whose message is
	// "<signal> signal received", such as "interrupt signal received".
	//
	// os.Kill and SIGSTOP cannot be caught, so listing them has no effect.
	// stop must be called, typically with defer, to release the
	// registration. A signal that arrives after stop is called gets its
	// default action again, which for an interrupt is to exit.
	NotifyContext func(parent context.Context, signals ...os.Signal) (ctx context.Context, stop context.CancelFunc)
}

Process is what exec hands a process: its arguments, its environment, its standard streams and the delivery of its signals. It is the one value through which an App reaches the OS process it runs in.

Every field is required, and the zero Process is invalid. This is documented rather than checked at runtime: a complete Process comes from OSProcess in production and from a test fake in tests, so code that receives a Process may use any field without a nil check.

func OSProcess added in v0.22.0

func OSProcess() Process

OSProcess returns the Process of the current OS process: os.Args, os.Stdin, os.Stdout, os.Stderr, OSEnvironment{} and signal.NotifyContext. Every field is set.

type StaticEnvironment added in v0.22.0

type StaticEnvironment struct {
	// contains filtered or unexported fields
}

StaticEnvironment is an Environment backed by a fixed set of values. It is the Environment of a hand-built Process in tests.

A StaticEnvironment is immutable, so it is safe for concurrent use. Build one with NewStaticEnvironment; the zero StaticEnvironment has no variables set.

func NewStaticEnvironment added in v0.22.0

func NewStaticEnvironment(values map[string]string) StaticEnvironment

NewStaticEnvironment returns a StaticEnvironment holding a copy of values. Changing values afterwards does not change the StaticEnvironment.

func (StaticEnvironment) Value added in v0.22.0

func (e StaticEnvironment) Value(name string) (string, error)

Value implements the Environment interface.

type ValueNotFoundError added in v0.22.0

type ValueNotFoundError struct {
	// Name is the name of the variable that is not set.
	Name string
}

ValueNotFoundError is returned by an Environment when a variable is not set.

func (ValueNotFoundError) Error added in v0.22.0

func (e ValueNotFoundError) Error() string

Error implements the error interface.

Directories

Path Synopsis
Package bedrocktest provides a hermetic bedrock.Process for tests that run a whole bedrock.App, in the way net/http/httptest provides servers for tests of HTTP handlers.
Package bedrocktest provides a hermetic bedrock.Process for tests that run a whole bedrock.App, in the way net/http/httptest provides servers for tests of HTTP handlers.
circuit module
Package config reads configuration values that are composed by chaining.
Package config reads configuration values that are composed by chaining.
example
httpserver command
Httpserver is a complete HTTP service built on bedrock with the standard library alone.
Httpserver is a complete HTTP service built on bedrock with the standard library alone.
fit module
host module
retry module

Jump to

Keyboard shortcuts

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