Dependency Injection in Go: Patterns & Best Practices
Master DI patterns for testable Go code
Dependency injection (DI) is a fundamental design pattern that promotes clean, testable, and maintainable code in Go applications.
Whether you’re building REST APIs, implementing multi-tenant database patterns, or working with ORM libraries, understanding dependency injection will significantly improve your code quality.

What is Dependency Injection?
Dependency injection is a design pattern where components receive their dependencies from external sources rather than creating them internally. This approach decouples components, making your code more modular, testable, and maintainable.
In Go, dependency injection is particularly powerful because of the language’s interface-based design philosophy. Go’s implicit interface satisfaction means you can easily swap implementations without modifying existing code.
Why Use Dependency Injection in Go?
Improved Testability: By injecting dependencies, you can easily replace real implementations with mocks or test doubles. This allows you to write unit tests that are fast, isolated, and don’t require external services like databases or APIs.
Better Maintainability: Dependencies become explicit in your code. When you look at a constructor function, you immediately see what a component requires. This makes the codebase easier to understand and modify.
Loose Coupling: Components depend on abstractions (interfaces) rather than concrete implementations. This means you can change implementations without affecting dependent code.
Flexibility: You can configure different implementations for different environments (development, testing, production) without changing your business logic.
Constructor Injection: The Go Way
The most common and idiomatic way to implement dependency injection in Go is through constructor functions. These are typically named NewXxx and accept dependencies as parameters.
Basic Example
Here’s a simple example demonstrating constructor injection:
// Define an interface for the repository
type UserRepository interface {
FindByID(id int) (*User, error)
Save(user *User) error
}
// Service depends on the repository interface
type UserService struct {
repo UserRepository
}
// Constructor injects the dependency
func NewUserService(repo UserRepository) *UserService {
return &UserService{repo: repo}
}
// Methods use the injected dependency
func (s *UserService) GetUser(id int) (*User, error) {
return s.repo.FindByID(id)
}
This pattern makes it clear that UserService requires a UserRepository. You cannot create a UserService without providing a repository, which prevents runtime errors from missing dependencies.
Multiple Dependencies
When a component has multiple dependencies, simply add them as constructor parameters:
type EmailService interface {
Send(to, subject, body string) error
}
type Logger interface {
Info(msg string)
Error(msg string, err error)
}
type OrderService struct {
repo OrderRepository
emailSvc EmailService
logger Logger
paymentSvc PaymentService
}
func NewOrderService(
repo OrderRepository,
emailSvc EmailService,
logger Logger,
paymentSvc PaymentService,
) *OrderService {
return &OrderService{
repo: repo,
emailSvc: emailSvc,
logger: logger,
paymentSvc: paymentSvc,
}
}
Interface Design for Dependency Injection
One of the key principles when implementing dependency injection is the Dependency Inversion Principle (DIP): high-level modules should not depend on low-level modules; both should depend on abstractions.
In Go, this means defining small, focused interfaces that represent only what your component needs:
// Good: Small, focused interface
type PaymentProcessor interface {
ProcessPayment(amount float64) error
}
// Bad: Large interface with unnecessary methods
type PaymentService interface {
ProcessPayment(amount float64) error
RefundPayment(id string) error
GetPaymentHistory(userID int) ([]Payment, error)
UpdatePaymentMethod(userID int, method PaymentMethod) error
// ... many more methods
}
The smaller interface follows the Interface Segregation Principle—clients shouldn’t depend on methods they don’t use. This makes your code more flexible and easier to test.
Real-World Example: Database Abstraction
When working with databases in Go applications, you’ll often need to abstract database operations. Here’s how dependency injection helps:
// Database interface - high level abstraction
type DB interface {
Query(ctx context.Context, query string, args ...interface{}) (Rows, error)
Exec(ctx context.Context, query string, args ...interface{}) (Result, error)
BeginTx(ctx context.Context) (Tx, error)
}
// Repository depends on the abstraction
type UserRepository struct {
db DB
}
func NewUserRepository(db DB) *UserRepository {
return &UserRepository{db: db}
}
func (r *UserRepository) FindByID(ctx context.Context, id int) (*User, error) {
query := "SELECT id, name, email FROM users WHERE id = $1"
rows, err := r.db.Query(ctx, query, id)
if err != nil {
return nil, err
}
defer rows.Close()
// ... parse rows
}
This pattern is especially useful when implementing multi-tenant database patterns, where you might need to switch between different database implementations or connection strategies.
The Composition Root Pattern
The Composition Root is where you assemble all your dependencies at the application’s entry point (typically main). This centralizes dependency configuration and makes the dependency graph explicit.
func main() {
// Initialize infrastructure dependencies
db := initDatabase()
logger := initLogger()
// Initialize repositories
userRepo := NewUserRepository(db)
orderRepo := NewOrderRepository(db)
// Initialize services with dependencies
emailSvc := NewEmailService(logger)
paymentSvc := NewPaymentService(logger)
userSvc := NewUserService(userRepo, logger)
orderSvc := NewOrderService(orderRepo, emailSvc, logger, paymentSvc)
// Initialize HTTP handlers
userHandler := NewUserHandler(userSvc)
orderHandler := NewOrderHandler(orderSvc)
// Wire up routes
router := setupRouter(userHandler, orderHandler)
// Start server
log.Fatal(http.ListenAndServe(":8080", router))
}
This approach makes it clear how your application is structured and where dependencies come from. It’s particularly valuable when building REST APIs in Go, where you need to coordinate multiple layers of dependencies.
Dependency Injection Frameworks
For larger applications with complex dependency graphs, managing dependencies manually can become cumbersome. Go has several DI frameworks that can help:
Google Wire (Compile-Time DI)
Wire is a compile-time dependency injection tool that generates code. It’s type-safe and has no runtime overhead.
Installation:
go install github.com/google/wire/cmd/wire@latest
Example:
// wire.go
//go:build wireinject
// +build wireinject
package main
import "github.com/google/wire"
func InitializeApp() (*App, error) {
wire.Build(
NewDB,
NewUserRepository,
NewUserService,
NewUserHandler,
NewApp,
)
return &App{}, nil
}
Wire generates the dependency injection code at compile time, ensuring type safety and eliminating runtime reflection overhead.
Uber Dig (Runtime DI)
Dig is a runtime dependency injection framework that uses reflection. It’s more flexible but has some runtime cost.
Example:
import "go.uber.org/dig"
func main() {
container := dig.New()
// Register providers
container.Provide(NewDB)
container.Provide(NewUserRepository)
container.Provide(NewUserService)
container.Provide(NewUserHandler)
// Invoke function that needs dependencies
err := container.Invoke(func(handler *UserHandler) {
// Use handler
})
if err != nil {
log.Fatal(err)
}
}
When to Use Frameworks
Use a framework when:
- Your dependency graph is complex with many interdependent components
- You have multiple implementations of the same interface that need to be selected based on configuration
- You want automatic dependency resolution
- You’re building a large application where manual wiring becomes error-prone
Stick with manual DI when:
- Your application is small to medium-sized
- The dependency graph is simple and easy to follow
- You want to keep dependencies minimal and explicit
- You prefer explicit code over generated code
Testing with Dependency Injection
One of the primary benefits of dependency injection is improved testability. Here’s how DI makes testing easier:
Unit Testing Example
// Mock implementation for testing
type mockUserRepository struct {
users map[int]*User
err error
}
func (m *mockUserRepository) FindByID(id int) (*User, error) {
if m.err != nil {
return nil, m.err
}
return m.users[id], nil
}
func (m *mockUserRepository) Save(user *User) error {
if m.err != nil {
return m.err
}
m.users[user.ID] = user
return nil
}
// Test using the mock
func TestUserService_GetUser(t *testing.T) {
mockRepo := &mockUserRepository{
users: map[int]*User{
1: {ID: 1, Name: "John", Email: "john@example.com"},
},
}
service := NewUserService(mockRepo)
user, err := service.GetUser(1)
assert.NoError(t, err)
assert.Equal(t, "John", user.Name)
}
This test runs quickly, doesn’t require a database, and tests your business logic in isolation. When working with ORM libraries in Go, you can inject mock repositories to test service logic without database setup.
Common Patterns and Best Practices
1. Use Interface Segregation
Keep interfaces small and focused on what the client actually needs:
// Good: Client only needs to read users
type UserReader interface {
FindByID(id int) (*User, error)
FindByEmail(email string) (*User, error)
}
// Separate interface for writing
type UserWriter interface {
Save(user *User) error
Delete(id int) error
}
2. Return Errors from Constructors
Constructors should validate dependencies and return errors if initialization fails:
func NewUserService(repo UserRepository) (*UserService, error) {
if repo == nil {
return nil, errors.New("user repository cannot be nil")
}
return &UserService{repo: repo}, nil
}
3. Use Context for Request-Scoped Dependencies
For dependencies that are request-specific (like database transactions), pass them via context:
type ctxKey string
const dbKey ctxKey = "db"
func WithDB(ctx context.Context, db DB) context.Context {
return context.WithValue(ctx, dbKey, db)
}
func DBFromContext(ctx context.Context) (DB, bool) {
db, ok := ctx.Value(dbKey).(DB)
return db, ok
}
4. Avoid Over-Injection
Don’t inject dependencies that are truly internal implementation details. If a component creates and manages its own helper objects, that’s fine:
// Good: Internal helper doesn't need injection
type UserService struct {
repo UserRepository
// Internal cache - doesn't need injection
cache map[int]*User
}
func NewUserService(repo UserRepository) *UserService {
return &UserService{
repo: repo,
cache: make(map[int]*User),
}
}
5. Document Dependencies
Use comments to document why dependencies are needed and any constraints:
// UserService handles user-related business logic.
// It requires a UserRepository for data access and a Logger for
// error tracking. The repository must be thread-safe if used
// in concurrent contexts.
func NewUserService(repo UserRepository, logger Logger) *UserService {
// ...
}
When NOT to Use Dependency Injection
Dependency injection is a powerful tool, but it’s not always necessary:
Skip DI for:
- Simple value objects or data structures
- Internal helper functions or utilities
- One-off scripts or small utilities
- When direct instantiation is clearer and simpler
Example of when NOT to use DI:
// Simple struct - no need for DI
type Point struct {
X, Y float64
}
func NewPoint(x, y float64) Point {
return Point{X: x, Y: y}
}
// Simple utility - no need for DI
func FormatCurrency(amount float64) string {
return fmt.Sprintf("$%.2f", amount)
}
Integration with Go Ecosystem
Dependency injection works seamlessly with other Go patterns and tools. When building applications that use Go’s standard library for web scraping or generating PDF reports, you can inject these services into your business logic:
type PDFGenerator interface {
GenerateReport(data ReportData) ([]byte, error)
}
type ReportService struct {
pdfGen PDFGenerator
repo ReportRepository
}
func NewReportService(pdfGen PDFGenerator, repo ReportRepository) *ReportService {
return &ReportService{
pdfGen: pdfGen,
repo: repo,
}
}
This allows you to swap PDF generation implementations or use mocks during testing.
Conclusion
Dependency injection is a cornerstone of writing maintainable, testable Go code. By following the patterns outlined in this article—constructor injection, interface-based design, and the composition root pattern—you’ll create applications that are easier to understand, test, and modify.
Start with manual constructor injection for small to medium applications, and consider frameworks like Wire or Dig as your dependency graph grows. Remember that the goal is clarity and testability, not complexity for its own sake.
For more Go development resources, check out our Go Cheatsheet for quick reference on Go syntax and common patterns.
Useful links
- Go Cheatsheet
- Beautiful Soup Alternatives for Go
- Generating PDF in GO - Libraries and examples
- Multi-Tenancy Database Patterns with examples in Go
- ORM to use in GO: GORM, sqlc, Ent or Bun?
- Building REST APIs in Go: Complete Guide
External Resources
- How to Use Dependency Injection in Go - freeCodeCamp
- Best Practices for Dependency Inversion in Golang - Relia Software
- Practical Guide to Dependency Injection in Go - Relia Software
- Google Wire - Compile-time Dependency Injection
- Uber Dig - Runtime Dependency Injection
- SOLID Principles in Go - Software Patterns Lexicon