Migrating from Google Wire
This guide will help you migrate your dependency injection setup from Google Wire to samber/do
.
Overview
Google Wire and samber/do
are both dependency injection libraries for Go, but they have different approaches:
- Google Wire: Uses code generation with
//go:build wire
directives - samber/do: Uses runtime dependency injection with a fluent API
Key Differences
Feature | Google Wire | samber/do |
---|---|---|
Approach | Code generation | Runtime injection |
Build time | Requires wire command | No build tools needed |
Flexibility | Static, compile-time | Dynamic, runtime |
Error handling | Compile-time errors | Runtime errors |
Service lifecycle | Limited | Full lifecycle management |
Migration Steps
1. Remove Wire Dependencies
Remove Wire from your go.mod
:
go mod edit -droprequire github.com/google/wire
Remove any Wire-related build tags and imports from your code.
2. Replace Wire Provider Functions
Before (Wire):
// +build wireinject
package main
import (
"github.com/google/wire"
"myapp/services"
)
func InitializeApp() (*App, error) {
wire.Build(
services.NewDatabase,
services.NewUserService,
services.NewApp,
)
return &App{}, nil
}
After (samber/do):
package main
import (
"github.com/samber/do/v2"
"myapp/services"
)
func InitializeApp() (*App, error) {
injector := do.New()
// Register services
do.Provide(injector, services.NewDatabase)
do.Provide(injector, services.NewUserService)
do.Provide(injector, services.NewApp)
// Invoke the app
app, err := do.Invoke[*App](injector)
if err != nil {
return nil, err
}
return app, nil
}
3. Update Service Constructors
Wire provider functions need to be updated for samber/do
. Service constructors receive do.Injector
as the first parameter and return an additional error:
Before (Wire):
func NewUserService(db *Database) *UserService {
return &UserService{db: db}
}
func NewDatabase(config *Config) *Database {
return &Database{config: config}
}
After (samber/do):
func NewUserService(i do.Injector) (*UserService, error) {
db := do.MustInvoke[*Database](i)
return &UserService{db: db}, nil
}
func NewDatabase(i do.Injector) (*Database, error) {
config := do.MustInvoke[*Config](i)
return &Database{config: config}, nil
}
4. Handle Interface Bindings
Before (Wire):
var Set = wire.NewSet(
wire.Bind(new(Repository), new(*UserRepository)),
NewUserRepository,
NewUserService,
)
After (samber/do):
injector := do.New()
do.Provide(injector, func(i do.Injector) (Repository, error) {
return &UserRepository{}, nil
})
Or declare an explicit binding:
// Register the concrete implementation
do.Provide(injector, func(i do.Injector) (*UserRepository*, error) {
return &UserRepository{}, nil
})
// Register the interface binding
do.As[*UserRepository, Repository](i)
Or bind on service loading:
// Register the concrete implementation
do.Provide(injector, func(i do.Injector) (*UserRepository, error) {
repo := do.MustInvoke[*UserRepository](i)
return repo, nil
})
// Find the matching concrete type during invocation
userRepository, err := do.InvokeAs[Repository](i)
5. Update Main Function
Before (Wire):
func main() {
app, err := InitializeApp()
if err != nil {
log.Fatal(err)
}
app.Run()
}
After (samber/do):
func main() {
app, err := InitializeApp()
if err != nil {
log.Fatal(err)
}
// Optional: Graceful shutdown
defer app.Shutdown()
app.Run()
}
Advanced Migration Patterns
Provider Sets
Before (Wire):
var UserSet = wire.NewSet(
NewUserRepository,
NewUserService,
)
var AppSet = wire.NewSet(
UserSet,
NewDatabase,
NewApp,
)
After (samber/do):
var UserPackage = do.Package(
do.Lazy(NewUserRepository),
do.Lazy(NewUserService),
)
var AppPackage = do.Package(
do.Lazy(UserPackage),
do.Lazy(NewDatabase),
do.Lazy(NewApp),
)
Conditional Providers
Before (Wire):
func InitializeApp(env string) (*App, error) {
if env == "test" {
wire.Build(TestSet)
} else {
wire.Build(ProdSet)
}
return &App{}, nil
}
After (samber/do):
func InitializeApp(env string) (*App, error) {
injector := do.New()
if env == "test" {
RegisterTestServices(injector)
} else {
RegisterProdServices(injector)
}
app := do.MustInvoke[*App](injector)
return app, nil
}
Benefits of Migration
- No build tools required - No need to run
wire
command - Runtime flexibility - Can change dependencies at runtime
- Better error handling - More detailed error messages
- Service lifecycle management - Built-in shutdown and health checks
- Scoped injection - Support for request-scoped services
- Lazy loading - Services are created only when needed
Common Pitfalls
- Missing dependencies - Ensure all required services are registered
- Circular dependencies -
samber/do
will detect and report these - Interface bindings - Use
do.As
for interface implementations - Error handling - Use
do.MustInvoke
instead ofdo.Invoke
as runtime errors and handled automatically
Testing
Your existing tests should work with minimal changes:
func TestUserService(t *testing.T) {
testInjector.Clone()
do.OverrideNamed["*UserService"](func(i *Injector) (*MockUserService, error) {
// ...
})
service := do.MustInvoke[*UserService](injector)
// Your test logic here
}
Next Steps
After migration, consider exploring these samber/do
features: