Core Concepts
Let’s understand godi’s core concepts through real examples. No fluff, just what you need to know.
Services: Your Application Building Blocks
A service is any type that does something useful in your app:
// This is a service - it sends emails
type EmailService struct {
smtp SMTPClient
}
// This is also a service - it logs things
type Logger interface {
Log(message string)
}
// Services can be interfaces or structs - your choice!
Constructors: How Services Are Created
A constructor is just a function that creates your service:
// Constructor for EmailService
func NewEmailService(smtp SMTPClient) *EmailService {
return &EmailService{smtp: smtp}
}
// Constructor for Logger
func NewLogger(config *Config) Logger {
return &FileLogger{
path: config.LogPath,
}
}
// godi calls these functions and provides the parameters automatically!
The Container: Where Everything Comes Together
Think of godi as having two main parts:
1. ServiceCollection - The Recipe Book
This is where you tell godi about your services:
services := godi.NewServiceCollection()
// Tell godi about your services
services.AddSingleton(NewLogger) // "Here's how to make a Logger"
services.AddScoped(NewEmailService) // "Here's how to make an EmailService"
2. ServiceProvider - The Kitchen
This is what actually creates and manages your services:
// Build the provider from your collection
provider, _ := services.BuildServiceProvider()
// Now you can get your services
logger, _ := godi.Resolve[Logger](provider)
emailService, _ := godi.Resolve[*EmailService](provider)
Service Lifetimes: When Things Are Created
This is the most important concept in godi. There are three lifetimes:
Singleton - One for the Whole App
services.AddSingleton(NewLogger)
// Created once, reused everywhere
logger1, _ := godi.Resolve[Logger](provider) // Creates new logger
logger2, _ := godi.Resolve[Logger](provider) // Returns SAME logger
// logger1 == logger2
Use for: Database connections, loggers, configuration, caches
Scoped - One per “Operation”
services.AddScoped(NewShoppingCart)
// In web apps, typically one per HTTP request
scope1 := provider.CreateScope(ctx)
cart1, _ := godi.Resolve[*ShoppingCart](scope1.ServiceProvider())
scope2 := provider.CreateScope(ctx)
cart2, _ := godi.Resolve[*ShoppingCart](scope2.ServiceProvider())
// cart1 != cart2 (different scopes = different instances)
Use for: Database transactions, request context, user sessions
Transient - New Every Time
services.AddTransient(NewEmailMessage)
// Always creates a new instance
email1, _ := godi.Resolve[*EmailMessage](provider)
email2, _ := godi.Resolve[*EmailMessage](provider)
// email1 != email2 (always different)
Use for: Unique operations, temporary objects, messages
Scopes: Isolation for Operations
A scope creates a boundary for scoped services. Think of it as a “bubble”:
// Web request example
func HandleRequest(provider godi.ServiceProvider) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Create scope for this request
scope := provider.CreateScope(r.Context())
defer scope.Close()
// Everything in this scope shares the same instances
repo, _ := godi.Resolve[*UserRepository](scope.ServiceProvider())
service, _ := godi.Resolve[*UserService](scope.ServiceProvider())
// Both repo and service share the same transaction!
}
}
Real Example: Putting It All Together
Let’s see how these concepts work in a real app:
package main
import (
"context"
"database/sql"
"fmt"
"github.com/junioryono/godi"
)
// 1. Define your services
type Logger interface {
Log(msg string)
}
type Database struct {
conn *sql.DB
}
type Transaction struct {
tx *sql.Tx
}
type UserRepository struct {
tx *Transaction
}
type EmailService struct {
logger Logger
}
type UserService struct {
repo *UserRepository
email *EmailService
tx *Transaction
}
// 2. Create constructors
func NewLogger() Logger {
return &ConsoleLogger{}
}
func NewDatabase() *Database {
conn, _ := sql.Open("sqlite3", ":memory:")
return &Database{conn: conn}
}
func NewTransaction(db *Database) *Transaction {
tx, _ := db.conn.Begin()
return &Transaction{tx: tx}
}
func NewUserRepository(tx *Transaction) *UserRepository {
return &UserRepository{tx: tx}
}
func NewEmailService(logger Logger) *EmailService {
return &EmailService{logger: logger}
}
func NewUserService(repo *UserRepository, email *EmailService, tx *Transaction) *UserService {
return &UserService{repo: repo, email: email, tx: tx}
}
// 3. Wire everything up
func main() {
// Configure services
services := godi.NewServiceCollection()
// Singletons - shared across app
services.AddSingleton(NewLogger)
services.AddSingleton(NewDatabase)
services.AddSingleton(NewEmailService)
// Scoped - per operation
services.AddScoped(NewTransaction)
services.AddScoped(NewUserRepository)
services.AddScoped(NewUserService)
// Build container
provider, _ := services.BuildServiceProvider()
defer provider.Close()
// Simulate handling a request
handleUserCreation(provider)
}
func handleUserCreation(provider godi.ServiceProvider) {
// Create scope for this operation
scope := provider.CreateScope(context.Background())
defer scope.Close() // Transaction rollback if not committed
// Get user service - everything is wired automatically!
userService, _ := godi.Resolve[*UserService](scope.ServiceProvider())
// Use it
userService.CreateUser("john@example.com")
userService.tx.Commit() // Explicit commit
}
The Magic of Dependency Injection
Notice what we DIDN’T have to do:
❌ Manually create each service in the right order
❌ Pass dependencies through multiple layers
❌ Worry about cleanup/disposal
❌ Handle transaction passing
godi handled all of that for us!
Quick Reference
Concept |
What It Is |
When to Use |
|---|---|---|
Service |
Any type that does work |
Everything in your app |
Constructor |
Function that creates a service |
One per service type |
ServiceCollection |
Where you register services |
Once at startup |
ServiceProvider |
What creates service instances |
Throughout your app |
Singleton |
One instance forever |
Shared resources |
Scoped |
One instance per scope |
Request-specific data |
Transient |
New instance each time |
Temporary objects |
Scope |
Boundary for scoped services |
Per request/operation |
Next Steps
Now that you understand the basics:
Try the Getting Started Tutorial
Learn about Scoped Services in Detail
Understand When to Use Modules
Remember: Start simple, add complexity only when needed!