Vessel is a powerful, type-safe dependency injection container for Go, built as part of the Forge framework. It provides elegant service lifecycle management, flexible dependency resolution, and comprehensive testing support.
- ποΈ Constructor Injection - Uber dig-style constructor-based DI with automatic dependency resolution using
ProvideConstructor - π Typed Service Keys - Strongly-typed service keys with
ServiceKey[T]for compile-time safety and IDE autocomplete - πͺ Middleware System - Hook into service resolution and lifecycle events for logging, metrics, and validation
- π Batch Registration - Register multiple services efficiently with
RegisterServices()and typed variants - π Service Discovery - Query and filter services with
Query(),FindByGroup(), andFindByLifecycle() - π¦ Enhanced Scopes - Scope context storage with
SetScoped()/GetScoped()for request-specific data - π¨ Sentinel Errors - Proper error handling with exported sentinel errors for
errors.Is()checking
- π― Type-Safe Generics - Compile-time type safety with Go generics
- ποΈ Constructor Injection - Uber dig-style automatic dependency resolution
- π Typed Service Keys - Strongly-typed service keys for compile-time safety
- π Multiple Lifecycles - Singleton, Transient, and Scoped services
- β‘ Lazy Dependencies - Defer expensive service initialization
- π Typed Injection - Automatic dependency resolution with type checking
- π Service Lifecycle - Built-in Start/Stop/Health management
- π Circular Detection - Automatic circular dependency detection
- π§΅ Concurrency Safe - Thread-safe container operations
- π¦ Request Scoping - Perfect for HTTP request-scoped services with context storage
- π Interface Binding - Register implementations as interfaces
- πͺ Middleware Hooks - Intercept resolve, start, and lifecycle events
- π Service Discovery - Query and filter services by criteria
- π Batch Registration - Register multiple services efficiently
- π§ͺ Test Friendly - Easy mocking and testing utilities
go get github.com/xraph/vesselpackage main
import (
"context"
"fmt"
"github.com/xraph/vessel"
)
type Database struct {
connectionString string
}
func (d *Database) Name() string { return "database" }
func (d *Database) Start(ctx context.Context) error {
fmt.Println("Connecting to database...")
return nil
}
func (d *Database) Stop(ctx context.Context) error {
fmt.Println("Closing database connection...")
return nil
}
type UserService struct {
db *Database
}
func main() {
// Create a new container
c := vessel.New()
// Register services
vessel.RegisterSingleton(c, "database", func(c vessel.Vessel) (*Database, error) {
return &Database{connectionString: "postgres://..."}, nil
})
vessel.RegisterSingleton(c, "userService", func(c vessel.Vessel) (*UserService, error) {
db := vessel.Must[*Database](c, "database")
return &UserService{db: db}, nil
})
// Start all services
ctx := context.Background()
if err := c.Start(ctx); err != nil {
panic(err)
}
defer c.Stop(ctx)
// Resolve and use services
userService := vessel.Must[*UserService](c, "userService")
fmt.Printf("User service ready: %v\n", userService)
}Vessel supports three lifecycle types:
Created once and shared across the entire application.
vessel.RegisterSingleton(c, "config", func(c vessel.Vessel) (*Config, error) {
return LoadConfig(), nil
})Created new every time it's resolved.
vessel.RegisterTransient(c, "request", func(c vessel.Vessel) (*Request, error) {
return &Request{ID: uuid.New()}, nil
})Created once per scope, perfect for HTTP requests.
vessel.RegisterScoped(c, "session", func(c vessel.Vessel) (*Session, error) {
return &Session{StartTime: time.Now()}, nil
})
// In HTTP handler
scope := c.BeginScope()
defer scope.End()
session, _ := vessel.ResolveScope[*Session](scope, "session")Scopes now support context storage for request-specific data:
scope := c.BeginScope()
defer scope.End()
// Store request-specific context
vessel.SetScoped(scope, "requestID", "abc-123")
vessel.SetScoped(scope, "user", currentUser)
// Retrieve typed values
requestID, ok := vessel.GetScoped[string](scope, "requestID")
user, ok := vessel.GetScoped[*User](scope, "user")
// Check scope status
if !scope.(*vessel.Scope).IsEnded() {
// Scope is still active
}
// List services resolved in this scope
services := scope.(*vessel.Scope).Services()Use strongly-typed service keys for compile-time safety and IDE autocomplete:
// Define typed service keys
var (
DatabaseKey = vessel.NewServiceKey[*Database]("database")
UserServiceKey = vessel.NewServiceKey[*UserService]("userService")
LoggerKey = vessel.NewServiceKey[Logger]("logger")
)
// Register with type safety
vessel.RegisterWithKey(c, DatabaseKey, func(c vessel.Vessel) (*Database, error) {
return &Database{}, nil
}, vessel.Singleton())
vessel.RegisterWithKey(c, UserServiceKey, func(c vessel.Vessel) (*UserService, error) {
db := vessel.MustWithKey(c, DatabaseKey) // Type-safe!
return &UserService{db: db}, nil
})
// Resolve with full type safety and autocomplete
db, err := vessel.ResolveWithKey(c, DatabaseKey)
// db is *Database, no type assertion needed!
// Or use Must variant
userService := vessel.MustWithKey(c, UserServiceKey)
// Check if service exists
if vessel.HasKey(c, DatabaseKey) {
// Service is registered
}Vessel supports Uber dig-style constructor-based dependency injection with automatic resolution:
// Simple constructor - dependencies are automatically resolved by type
type Database struct{}
type Logger struct{}
type UserService struct {
db *Database
log *Logger
}
func NewDatabase() *Database {
return &Database{}
}
func NewLogger() *Logger {
return &Logger{}
}
func NewUserService(db *Database, log *Logger) *UserService {
return &UserService{db: db, log: log}
}
c := vessel.New()
// Register constructors - dependencies are automatically resolved
vessel.ProvideConstructor(c, NewDatabase)
vessel.ProvideConstructor(c, NewLogger)
vessel.ProvideConstructor(c, NewUserService)
// Resolve by type
userService, err := vessel.InjectType[*UserService](c)// Named services
vessel.ProvideConstructor(c, NewPrimaryDB, vessel.WithName("primary"))
vessel.ProvideConstructor(c, NewSecondaryDB, vessel.WithName("secondary"))
// Resolve named services
primary, _ := vessel.InjectNamed[*Database](c, "primary")
secondary, _ := vessel.InjectNamed[*Database](c, "secondary")
// Check existence
if vessel.HasType[*Database](c) { /* ... */ }
if vessel.HasTypeNamed[*Database](c, "primary") { /* ... */ }
// Lifecycle options
vessel.ProvideConstructor(c, NewDB, vessel.AsSingleton()) // default
vessel.ProvideConstructor(c, NewReq, vessel.AsTransient())
vessel.ProvideConstructor(c, NewSession, vessel.AsScoped())Collect multiple implementations of the same type:
// Register multiple handlers in a group
vessel.ProvideConstructor(c, NewAuthHandler, vessel.AsGroup("handlers"))
vessel.ProvideConstructor(c, NewLoggingHandler, vessel.AsGroup("handlers"))
vessel.ProvideConstructor(c, NewMetricsHandler, vessel.AsGroup("handlers"))
// Inject all handlers as a slice
handlers, err := vessel.InjectGroup[Handler](c, "handlers")
// handlers is []Handler containing all three handlersRegister concrete types as interfaces:
type Writer interface {
Write([]byte) error
}
type FileWriter struct{}
func (f *FileWriter) Write(b []byte) error { return nil }
func NewFileWriter() *FileWriter {
return &FileWriter{}
}
// Register *FileWriter as Writer interface
vessel.ProvideConstructor(c, NewFileWriter, vessel.As(new(Writer)))
// Resolve by interface type
writer, err := vessel.InjectType[Writer](c)For constructors with many dependencies, use In and Out structs:
// Embed vessel.In for parameter objects
type ServiceParams struct {
vessel.In
DB *Database // Required dependency
Logger *Logger // Required dependency
Cache *Cache `optional:"true"` // Optional, nil if not registered
Primary *Database `name:"primary"` // Named dependency
}
func NewService(p ServiceParams) *Service {
return &Service{
db: p.DB,
logger: p.Logger,
cache: p.Cache,
primary: p.Primary,
}
}
vessel.ProvideConstructor(c, NewService)
// Embed vessel.Out for result objects (multiple results)
type ServiceResults struct {
vessel.Out
API *APIHandler
Web *WebHandler
GRPC *GRPCHandler
}
func NewHandlers(db *Database) ServiceResults {
return ServiceResults{
API: &APIHandler{db: db},
Web: &WebHandler{db: db},
GRPC: &GRPCHandler{db: db},
}
}
// All three handlers are registered and resolvable
vessel.ProvideConstructor(c, NewHandlers)
api, _ := vessel.InjectType[*APIHandler](c)
web, _ := vessel.InjectType[*WebHandler](c)Constructors can return errors:
func NewDatabase(cfg *Config) (*Database, error) {
if cfg.ConnectionString == "" {
return nil, errors.New("connection string required")
}
return &Database{conn: cfg.ConnectionString}, nil
}
// Error is returned when resolving
db, err := vessel.InjectType[*Database](c)
if err != nil {
// Handle constructor error
}// Panic if service not found or resolution fails
db := vessel.MustInjectType[*Database](c)
primary := vessel.MustInjectNamed[*Database](c, "primary")
handlers := vessel.MustInjectGroup[Handler](c, "handlers")Vessel automatically detects circular dependencies:
func NewA(b *B) *A { return &A{b: b} }
func NewB(a *A) *B { return &B{a: a} } // Circular!
vessel.ProvideConstructor(c, NewA)
vessel.ProvideConstructor(c, NewB)
_, err := vessel.InjectType[*A](c)
// Error: circular dependency detected: *A -> *B -> *AIntercept and observe service resolution and lifecycle events:
// Create logging middleware
loggingMiddleware := &vessel.FuncMiddleware{
BeforeResolveFunc: func(ctx context.Context, name string) error {
log.Printf("Resolving service: %s", name)
return nil
},
AfterResolveFunc: func(ctx context.Context, name string, service any, err error) error {
if err != nil {
log.Printf("Failed to resolve %s: %v", name, err)
} else {
log.Printf("Successfully resolved %s", name)
}
return nil
},
BeforeStartFunc: func(ctx context.Context, name string) error {
log.Printf("Starting service: %s", name)
return nil
},
AfterStartFunc: func(ctx context.Context, name string, err error) error {
if err != nil {
log.Printf("Failed to start %s: %v", name, err)
}
return nil
},
}
// Register middleware
c.(*vessel.ContainerImpl).Use(loggingMiddleware)
// Create custom middleware
type MetricsMiddleware struct {
metrics *Metrics
}
func (m *MetricsMiddleware) BeforeResolve(ctx context.Context, name string) error {
m.metrics.IncrementResolveCount(name)
return nil
}
func (m *MetricsMiddleware) AfterResolve(ctx context.Context, name string, service any, err error) error {
if err != nil {
m.metrics.IncrementResolveError(name)
}
return nil
}
// Implement other methods...Register multiple services efficiently:
// Batch register with untyped factories
err := vessel.RegisterServices(c,
vessel.Service("database", NewDatabase, vessel.Singleton()),
vessel.Service("cache", NewCache, vessel.Singleton()),
vessel.Service("logger", NewLogger, vessel.Singleton(), vessel.WithGroup("core")),
)
// Batch register with type safety
err := vessel.RegisterTypedServices(c,
vessel.TypedService("db", NewDatabase, vessel.Singleton()),
vessel.TypedService("cache", NewCache, vessel.Singleton()),
)
// Batch register with service keys
err := vessel.RegisterKeyedServices(c,
vessel.KeyedService(DatabaseKey, NewDatabase, vessel.Singleton()),
vessel.KeyedService(CacheKey, NewCache, vessel.Singleton()),
vessel.KeyedService(LoggerKey, NewLogger, vessel.Singleton()),
)Query and filter services by various criteria:
// Find all singleton services
singletons := vessel.FindByLifecycle(c, "singleton")
// Find all services in a group
apiHandlers := vessel.FindByGroup(c, "api-handlers")
// Find started services
started := vessel.FindStarted(c)
// Find not started services
notStarted := vessel.FindNotStarted(c)
// Complex queries
started := true
results := vessel.Query(c, vessel.ServiceQuery{
Lifecycle: "singleton",
Group: "api",
Metadata: map[string]string{
"version": "2.0",
"env": "production",
},
Started: &started,
})
// Get just the names
names := vessel.QueryNames(c, vessel.ServiceQuery{
Group: "background-workers",
})
for _, info := range results {
fmt.Printf("Found: %s (%s)\n", info.Name, info.Lifecycle)
}// Type-safe resolution with error handling
db, err := vessel.Resolve[*Database](c, "database")
if err != nil {
log.Fatal(err)
}
// Panic on error (use during startup)
db := vessel.Must[*Database](c, "database")Ensure a service and its dependencies are started before use:
// Resolves and starts the service if it implements di.Service
db, err := vessel.ResolveReady[*Database](ctx, c, "database")Use Provide for automatic dependency injection with type safety:
// Define dependencies with Inject
vessel.Provide[*UserService](c, "userService",
vessel.Inject[*Database]("database"),
vessel.Inject[*Logger]("logger"),
func(db *Database, log *Logger) (*UserService, error) {
return &UserService{
db: db,
logger: log,
}, nil
},
)
// Or use lifecycle-specific helpers
vessel.RegisterSingletonWith[*UserService](c, "userService",
vessel.Inject[*Database]("database"),
func(db *Database) (*UserService, error) {
return &UserService{db: db}, nil
},
)Break circular dependencies or defer expensive initialization:
type EmailService struct {
cache *vessel.Lazy[*Cache]
}
vessel.RegisterSingleton(c, "emailService", func(c vessel.Vessel) (*EmailService, error) {
return &EmailService{
cache: vessel.NewLazy[*Cache](c, "cache"),
}, nil
})
// Later, when needed:
func (s *EmailService) SendEmail(to string, body string) error {
cache, err := s.cache.Get() // Resolved on first access
if err != nil {
return err
}
// Use cache...
}vessel.Provide[*Service](c, "service",
vessel.LazyInject[*Cache]("cache"),
func(cache *vessel.Lazy[*Cache]) (*Service, error) {
return &Service{cache: cache}, nil
},
)
// With optional dependencies
vessel.Provide[*Service](c, "service",
vessel.OptionalInject[*Cache]("cache"),
func(cache *Cache) (*Service, error) {
// cache will be nil if not registered
return &Service{cache: cache}, nil
},
)Implement the di.Service interface for automatic lifecycle management:
type DatabaseService struct {
conn *sql.DB
}
func (d *DatabaseService) Name() string {
return "database"
}
func (d *DatabaseService) Start(ctx context.Context) error {
conn, err := sql.Open("postgres", "...")
if err != nil {
return err
}
d.conn = conn
return d.conn.PingContext(ctx)
}
func (d *DatabaseService) Stop(ctx context.Context) error {
return d.conn.Close()
}
// Optional: Health checks
func (d *DatabaseService) Health(ctx context.Context) error {
return d.conn.PingContext(ctx)
}
// Register and manage lifecycle
c := vessel.New()
vessel.RegisterSingleton(c, "database", func(c vessel.Vessel) (*DatabaseService, error) {
return &DatabaseService{}, nil
})
// Start all services in dependency order
ctx := context.Background()
c.Start(ctx)
// Check health of all services
c.Health(ctx)
// Stop all services in reverse order
c.Stop(ctx)Register implementations as interfaces:
type Logger interface {
Log(msg string)
}
type ConsoleLogger struct{}
func (c *ConsoleLogger) Log(msg string) {
fmt.Println(msg)
}
// Register implementation as interface
vessel.RegisterSingletonInterface[Logger, *ConsoleLogger](c, "logger",
func(c vessel.Vessel) (*ConsoleLogger, error) {
return &ConsoleLogger{}, nil
},
)
// Resolve as interface
logger := vessel.Must[Logger](c, "logger")
logger.Log("Hello, World!")Perfect for request-scoped resources with context storage:
func httpHandler(c vessel.Vessel) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Create a new scope for this request
scope := c.BeginScope()
defer scope.End() // Cleanup when done
// Store request-specific context
vessel.SetScoped(scope, "requestID", r.Header.Get("X-Request-ID"))
vessel.SetScoped(scope, "userID", getUserIDFromToken(r))
// Resolve scoped services
session, _ := vessel.ResolveScope[*Session](scope, "session")
userCtx, _ := vessel.ResolveScope[*UserContext](scope, "userContext")
// Retrieve context data in services
requestID, _ := vessel.GetScoped[string](scope, "requestID")
log.Printf("Handling request: %s", requestID)
// Use services...
// Services are automatically cleaned up when scope ends
}
}// Check if service is registered
if c.Has("database") {
// Service exists
}
// Check if service has been started
if c.IsStarted("database") {
// Service is running
}
// Get service information
info := c.Inspect("database")
fmt.Printf("Service: %s, Type: %s, Started: %v\n",
info.Name, info.Type, info.Started)
// List all registered services
services := c.Services()
for _, name := range services {
fmt.Println(name)
}Vessel makes testing easy with mock services:
func TestUserService(t *testing.T) {
c := vessel.New()
// Register mock database
vessel.RegisterSingleton(c, "database", func(c vessel.Vessel) (*MockDatabase, error) {
return &MockDatabase{
users: map[string]*User{
"1": {ID: "1", Name: "Test User"},
},
}, nil
})
vessel.RegisterSingleton(c, "userService", func(c vessel.Vessel) (*UserService, error) {
db := vessel.Must[*MockDatabase](c, "database")
return &UserService{db: db}, nil
})
// Test with mocked dependencies
service := vessel.Must[*UserService](c, "userService")
user, err := service.GetUser("1")
assert.NoError(t, err)
assert.Equal(t, "Test User", user.Name)
}Declare dependencies explicitly for better documentation and validation:
// With dependency tracking
c.Register("userService", func(c vessel.Vessel) (any, error) {
return &UserService{}, nil
}, vessel.WithDependencies("database", "logger"))
// Dependencies are validated at registration time
// and used for proper startup ordervessel.Provide[*HTTPClient](c, "httpClient",
vessel.Inject[*Config]("config"),
vessel.Inject[*Logger]("logger"),
func(cfg *Config, log *Logger) (*HTTPClient, error) {
return &HTTPClient{
timeout: cfg.HTTPTimeout,
logger: log,
}, nil
},
)Register pre-built instances:
config := &Config{Port: 8080}
vessel.RegisterValue(c, "config", config)Register multiple services in the same group for discovery:
// Register services with groups
vessel.RegisterSingleton(c, "handler1", ..., vessel.WithGroup("handlers"))
vessel.RegisterSingleton(c, "handler2", ..., vessel.WithGroup("handlers"))
vessel.RegisterSingleton(c, "handler3", ..., vessel.WithGroup("handlers"))
// Discover services by group
handlers := vessel.FindByGroup(c, "handlers")
for _, info := range handlers {
fmt.Printf("Found handler: %s\n", info.Name)
}
// Query services with metadata
vessel.RegisterSingleton(c, "worker1", ...,
vessel.WithGroup("workers"),
vessel.WithDIMetadata("priority", "high"),
)
highPriorityWorkers := vessel.Query(c, vessel.ServiceQuery{
Group: "workers",
Metadata: map[string]string{"priority": "high"},
})Vessel provides structured errors with sentinel values for easy checking:
service, err := vessel.Resolve[*Service](c, "unknown")
if err != nil {
// Check with errors.Is for sentinel errors
if errors.Is(err, vessel.ErrServiceNotFoundSentinel) {
log.Println("Service not registered")
}
if errors.Is(err, vessel.ErrCircularDependencySentinel) {
log.Println("Circular dependency detected")
}
if errors.Is(err, vessel.ErrScopeEnded) {
log.Println("Scope has ended")
}
if errors.Is(err, vessel.ErrTypeMismatchSentinel) {
log.Println("Type mismatch during resolution")
}
// Errors include contextual information
fmt.Printf("Error: %v\n", err)
}
// Check for specific error conditions
scope := c.BeginScope()
scope.End()
_, err = scope.Resolve("service")
if errors.Is(err, vessel.ErrScopeEnded) {
// Handle ended scope
}Vessel is optimized for production use:
BenchmarkResolve_Singleton_Cached-16 100M 12.00 ns/op 0 B/op 0 allocs/op
BenchmarkResolve_Transient-16 94M 12.78 ns/op 0 B/op 0 allocs/op
BenchmarkScope_Create-16 21M 56.46 ns/op 160 B/op 3 allocs/op
BenchmarkScope_Resolve_Cached-16 77M 15.60 ns/op 0 B/op 0 allocs/op
BenchmarkStart_10Services-16 351K 3.34 ΞΌs/op 6960 B/op 86 allocs/op
BenchmarkStart_100Services-16 45K 26.34 ΞΌs/op 58709 B/op 857 allocs/op
BenchmarkConcurrentResolve-16 8M 152 ns/op 0 B/op 0 allocs/op
BenchmarkConcurrentScope-16 7M 181 ns/op 448 B/op 4 allocs/op
Resolve_Singleton_Cached - Resolving an already-created singleton service. This is the most common operation in production. At ~12ns with zero allocations, it's essentially just a map lookup with a mutex read lock.
Resolve_Transient - Creating a new transient service instance each time. At ~13ns, the factory function is called but the service itself is simple (no dependencies), showing the framework's low overhead.
Scope_Create - Creating a new scope (e.g., for an HTTP request). At ~56ns with 160 bytes allocated, this is lightweight enough to create per-request without performance concerns.
Scope_Resolve_Cached - Resolving a scoped service that's already been created in the current scope. At ~16ns with zero allocations, subsequent resolutions within the same scope are very fast.
Start_10Services / Start_100Services - Starting services with lifecycle hooks. These scale linearly (~3.3ΞΌs for 10 services, ~26ΞΌs for 100 services), showing efficient startup even with many services. This happens once at application startup.
ConcurrentResolve - Multiple goroutines resolving the same singleton simultaneously. At ~152ns, the mutex contention is minimal, making Vessel safe for high-concurrency scenarios.
ConcurrentScope - Multiple goroutines creating and using separate scopes simultaneously. At ~181ns, isolated scopes have minimal contention, ideal for concurrent request handling.
- Cached singleton resolve: ~12ns (zero allocations) - The hot path for most applications
- Transient service creation: ~13ns (zero allocations) - Minimal framework overhead
- Scope creation: ~56ns (160 bytes, 3 allocations) - Efficient per-request scoping
- Scoped service resolve: ~16ns cached (zero allocations) - Fast repeated access within scope
- Thread-safe: Minimal contention under concurrent load (~10x slower than single-threaded)
- Startup: Linear scaling, ~260ns per service with lifecycle management
What This Means for Your Application:
- You can resolve services millions of times per second
- Creating scopes per HTTP request adds negligible overhead (~56ns)
- Concurrent access is safe and efficient for high-throughput services
- Startup time is predictable and scales with service count
- Register services at startup - Keep container immutable after initialization
- Use constructor injection - Prefer
ProvideConstructorfor cleaner, dig-like dependency resolution - Use typed service keys - Prefer
ServiceKey[T]over strings for type safety - Use generics for type safety - Avoid
anyand type assertions - Implement service lifecycle - Use Start/Stop for resource management
- Leverage scopes for requests - Create new scopes for HTTP handlers
- Use scope context storage - Store request-specific data with
SetScoped/GetScoped - Use In/Out structs for complex constructors - Embed
vessel.Inorvessel.Outfor many dependencies - Use lazy dependencies sparingly - Only for circular dependencies or expensive resources
- Declare dependencies explicitly - Use
WithDependencies()for documentation - Use middleware for cross-cutting concerns - Logging, metrics, security validation
- Query services for discovery - Use
Query()andFindByGroup()for dynamic service discovery - Batch register related services - Use
RegisterServices()for cleaner code - Test with mocks - Create fresh containers per test with mock services
// Before (wire)
//go:build wireinject
func InitializeApp() (*App, error) {
wire.Build(
NewDatabase,
NewUserService,
NewApp,
)
return nil, nil
}
// After (vessel)
func InitializeApp() (*App, error) {
c := vessel.New()
vessel.RegisterSingleton(c, "database", NewDatabase)
vessel.RegisterSingleton(c, "userService", NewUserService)
vessel.RegisterSingleton(c, "app", NewApp)
return vessel.Resolve[*App](c, "app")
}// Before (dig)
c := dig.New()
c.Provide(NewDatabase)
c.Provide(NewUserService)
c.Invoke(func(s *UserService) {
// use service
})
// After (vessel) - dig-style constructor injection
c := vessel.New()
vessel.ProvideConstructor(c, NewDatabase)
vessel.ProvideConstructor(c, NewUserService)
userService, _ := vessel.InjectType[*UserService](c)
// dig In/Out structs are fully supported
type Params struct {
dig.In // Change to vessel.In
DB *Database
}
// becomes
type Params struct {
vessel.In
DB *Database
}
// Optional, named, and group tags work the same way
type Deps struct {
vessel.In
Cache *Cache `optional:"true"`
Primary *DB `name:"primary"`
}
// Value groups
c.Provide(NewHandler, dig.Group("handlers")) // dig
vessel.ProvideConstructor(c, NewHandler, vessel.AsGroup("handlers")) // vessel
// Interface registration
c.Provide(NewFileWriter, dig.As(new(io.Writer))) // dig
vessel.ProvideConstructor(c, NewFileWriter, vessel.As(new(io.Writer))) // vesselpackage main
import (
"context"
"fmt"
"log"
"net/http"
"github.com/xraph/vessel"
)
// Domain types
type Config struct {
DatabaseURL string
Port int
}
type Database struct {
url string
}
func (d *Database) Name() string { return "database" }
func (d *Database) Start(ctx context.Context) error {
fmt.Printf("Connecting to %s\n", d.url)
return nil
}
func (d *Database) Stop(ctx context.Context) error {
fmt.Println("Closing database connection")
return nil
}
type UserRepository struct {
db *Database
}
type UserService struct {
repo *UserRepository
}
type HTTPServer struct {
container vessel.Vessel
port int
}
func (s *HTTPServer) Name() string { return "http-server" }
func (s *HTTPServer) Start(ctx context.Context) error {
fmt.Printf("Starting HTTP server on port %d\n", s.port)
return nil
}
func (s *HTTPServer) Stop(ctx context.Context) error {
fmt.Println("Shutting down HTTP server")
return nil
}
func main() {
// Initialize container
c := vessel.New()
// Register configuration
vessel.RegisterValue(c, "config", &Config{
DatabaseURL: "postgres://localhost/myapp",
Port: 8080,
})
// Register services with typed dependencies
vessel.RegisterSingletonWith[*Database](c, "database",
vessel.Inject[*Config]("config"),
func(cfg *Config) (*Database, error) {
return &Database{url: cfg.DatabaseURL}, nil
},
)
vessel.RegisterSingletonWith[*UserRepository](c, "userRepo",
vessel.Inject[*Database]("database"),
func(db *Database) (*UserRepository, error) {
return &UserRepository{db: db}, nil
},
)
vessel.RegisterSingletonWith[*UserService](c, "userService",
vessel.Inject[*UserRepository]("userRepo"),
func(repo *UserRepository) (*UserService, error) {
return &UserService{repo: repo}, nil
},
)
vessel.RegisterSingletonWith[*HTTPServer](c, "httpServer",
vessel.Inject[*Config]("config"),
func(cfg *Config) (*HTTPServer, error) {
return &HTTPServer{
container: c,
port: cfg.Port,
}, nil
},
)
// Start all services
ctx := context.Background()
if err := c.Start(ctx); err != nil {
log.Fatalf("Failed to start services: %v", err)
}
defer c.Stop(ctx)
// Application is running...
fmt.Println("Application started successfully!")
// Check health
if err := c.Health(ctx); err != nil {
log.Printf("Health check failed: %v", err)
}
}Contributions are welcome! Please feel free to submit a Pull Request.
This project is licensed under the MIT License - see the LICENSE file for details.
- Forge Framework - Complete application framework
- go-utils - Shared utilities for Go applications
- π« Issues: GitHub Issues
- π¬ Discussions: GitHub Discussions
- π Documentation: pkg.go.dev
Built with β€οΈ as part of the Forge ecosystem