fxbarrier
fxbarrier is a small utility library for the go.uber.org/fx dependency injection framework. It provides a mechanism to enforce execution order, ensuring that some components are initialized only after a central processing step is complete.
The Core Idea
In fx, constructors are executed lazily as needed, which makes it hard to enforce a strict startup order. For example, how do you ensure that an action like parsing command-line flags happens before any component that depends on those flag values is created?
fxbarrier solves this by introducing a named synchronization point, or "barrier".
-
fxbarrier.Barrier("my-phase", action): This creates a barrier. The action (e.g., parsing flags) is executed at a specific point in the startup process.
-
fxbarrier.Provide("my-phase", constructor): This tells fx that the constructor depends on the barrier. It will only be executed after the barrier's action has successfully completed.
Usage Example
The canonical problem is the "define-parse-use" sequence for command-line flags. fxbarrier can coordinate these steps perfectly.
Let's look at a complete example that implements a --version flag.
package main
import (
"flag"
"fmt"
"os"
"go.uber.org/fx"
"github.com/lftk/fxbarrier"
)
// A struct to hold our parsed flag values.
type flags struct {
Version bool
}
func main() {
app := fx.New(
// 1. Define: This constructor *defines* the flags.
// fxbarrier ensures it runs before the barrier's action,
// and makes its result (*flags) available only after the barrier is lifted.
fxbarrier.Provide("flags",
func(fs *flag.FlagSet) *flags {
var f flags
fs.BoolVar(&f.Version, "version", false, "show version")
return &f
},
),
// 2. Parse: This defines the "flags" barrier and its action.
// The action (flag.Parse) will run after all flag definitions
// from fxbarrier.Provide are complete.
fxbarrier.Barrier("flags",
func(fs *flag.FlagSet) error {
return fs.Parse(os.Args[1:])
},
),
// Provide the standard flag.CommandLine to the container.
fx.Supply(flag.CommandLine),
// 3. Use: This fx.Invoke block uses the *flags struct.
// It is guaranteed to run only after the flags have been defined and parsed.
fx.Invoke(func(f *flags) {
if f.Version {
fmt.Println("Version: v0.1.0")
os.Exit(0)
}
}),
// Use fx.NopLogger to keep the output clean.
fx.NopLogger,
)
app.Run()
}
How It Works
-
fxbarrier.Provide("flags", ...): We wrap the constructor that defines our flags (fs.BoolVar(...)). fxbarrier intercepts this. It ensures this constructor runs first, but it holds back its result (*flags) from the rest of the application.
-
fxbarrier.Barrier("flags", ...): This defines the barrier's action, which is to parse the flags (fs.Parse(...)). fxbarrier guarantees that this action runs after all constructors associated with the "flags" barrier have completed.
-
Barrier Lifted: Once the Parse action is successful, the barrier is "lifted". fxbarrier now releases the *flags struct into the fx container.
-
fx.Invoke(...): The Invoke function, which depends on *flags, can now finally run. It can safely use the f.Version field, knowing it has been properly parsed.
This creates a clear and safe execution flow: Define -> Parse -> Use.
Real-World Implementation
For a more complete, real-world implementation of flag parsing built on top of fxbarrier, see the flagfx library.