-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Functions and Methods
-
Section 4: Concurrency
-
Section 5: Packages and Tooling
-
Section 6: More Standard Library
-
Section 7: Building Services
-
Section 8: Advanced Go
-
Section 9: Go in the Cloud-Native Ecosystem
-
Section 10: Data Structures and Algorithms in Go
-
Section 11: Testing and Deployment
-
Section 12: Practical Projects
-
Section 13: More Standard Library Practice
-
Section 14: More Practice Projects
-
Section 15: Design Patterns in Go
-
Section 16: Interview Practice
-
Section 17: Package fmt In Depth
-
Section 18: Package strings and strconv
-
Section 19: Package os and io
-
Section 20: Package time
-
Section 21: Package sort and container
-
Section 22: Package encoding
-
Section 23: Package net/http In Depth
-
Section 24: Package context
-
Section 25: Package regexp and bytes
-
Section 26: Package errors In Depth
-
Section 27: Package crypto and hash
-
Section 28: Package flag and log
-
Section 29: Package sync In Depth
-
Section 30: More Practice Exercises
-
Section 31: Go Modules and Workspaces In Depth
-
Section 32: Generics Deep Dive (Go 1.18+)
-
Section 33: Testing Package In Depth
-
Section 34: More Interview and Whiteboard Practice
-
Section 35: Package math and unicode
-
Section 36: Package path and filepath
-
Section 37: Package database/sql
-
Section 38: Package text/template and html/template
-
Section 39: Package archive and compress
-
Section 40: Lower-Level net Package
-
Section 41: Package runtime and reflect
-
Section 42: Package embed and io/fs
-
Section 43: Go Assembly and CGO Basics
-
Section 44: Building CLIs and TUIs
-
Section 45: Go Performance Tuning
-
Section 46: More Real-World Projects
-
Section 47: Go in Production
-
Section 48: Go Design Patterns
-
Section 49: Go Interfaces Deep Dive
-
Section 50: Final Practice and Review
243: Practice Exercise: Building a Simple Dependency Injection Container
You've probably felt the pain of "constructor hell." It's that moment in a growing project where, to initialize a single UserService, you first have to initialize a UserRepository, which requires a DatabaseConnection, which requires a Config object. Before you've even written a line of business logic, you've spent twenty lines of code just wiring things together in main.go. It's tedious, and it makes your setup code brittle.
Think of a Dependency Injection (DI) container like a professional tool rental shop. If you're a contractor, you don't want to build a drill from scratch every time you need to put a screw in a wall. Instead, you go to the shop manager and say, "I need a drill." You don't care where the drill came from, who assembled it, or what battery is inside; you just want a working drill handed to you so you can get to work. The shop manager handles the inventory and the assembly—you just handle the task.
Mapping the Tool Shop to Go
In our code, the "Tool Shop Manager" is the Container. The "Inventory List" is a map that stores how to create specific types, and the "Request" is a method we'll call to retrieve those types. Here is how that maps out:
- The Shop Manager: A struct that holds a registry of "providers" (functions that know how to create the dependency).
- The Inventory List: A map where the key is the type of the service and the value is the function used to instantiate it.
- The Request: A
Resolvemethod that looks up the type in the map and executes the provider function.
Wiring the Container in Go
Go doesn't have the same kind of heavy reflection-based DI frameworks as Java or C#, and honestly, that's usually a good thing. We can build a lightweight version using reflect.Type as our key. I prefer this approach because it's explicit and avoids using string keys, which are magnets for typos.
package main
import (
"fmt"
"reflect"
)
// Provider is a function that returns a dependency
type Provider func() interface{}
// Container holds our mapping of types to their providers
type Container struct {
providers map[reflect.Type]Provider
}
func NewContainer() *Container {
return &Container{
providers: make(map[reflect.Type]Provider),
}
}
// Register tells the container how to create a specific type
func (c *Container) Register(target interface{}, provider Provider) {
t := reflect.TypeOf(target)
c.providers[t] = provider
}
// Resolve retrieves the dependency from the container
func (c *Container) Resolve(target interface{}) interface{} {
t := reflect.TypeOf(target)
if provider, ok := c.providers[t]; ok {
return provider()
}
panic(fmt.Sprintf("no provider registered for type: %s", t))
}
Putting the Registry to Work
The real magic happens when we use this to decouple our services. Let's say we have a Database and a UserRepository. Instead of manually passing the DB into the repo in main, we let the container handle the chain.
type Database struct{ ConnectionString string }
type UserRepository struct{ DB *Database }
func main() {
container := NewContainer()
// Register the Database
container.Register((*Database)(nil), func() interface{} {
return &Database{ConnectionString: "postgres://localhost:5432"}
})
// Register the UserRepository, resolving its dependency from the container
container.Register((*UserRepository)(nil), func() interface{} {
db := container.Resolve((*Database)(nil)).(*Database)
return &UserRepository{DB: db}
})
// Now we just ask for the Repo
repo := container.Resolve((*UserRepository)(nil)).(*UserRepository)
fmt.Println("Resolved repo with DB:", repo.DB.ConnectionString)
}
I'll be honest: the type assertions (the .(*Database) parts) are a bit clunky. That's the trade-off we make in Go for this kind of flexibility. In a production-grade system, you might use generics to wrap the Resolve method and hide those assertions, but for a practice exercise, seeing the raw machinery is more valuable.
📋 Practical Task
Exercise: Building an Email Notification System with DI
Your task is to expand the DI container concept to manage a notification system. You need to implement a system where a NotificationService depends on a MessageSender interface. This will prove that the container can handle interfaces, not just concrete structs.
Requirements:
- Define a
MessageSenderinterface with a methodSend(msg string) error. - Create two implementations of
MessageSender:EmailSenderandSmsSender. - Create a
NotificationServicestruct that requires aMessageSenderin its constructor. - Using the
Containerlogic from the lesson, register theEmailSenderas the provider for theMessageSenderinterface. - Resolve the
NotificationServiceand use it to send a message. - Bonus: Change a single line in your registration code to swap the
EmailSenderfor theSmsSenderwithout changing any logic inside theNotificationService.
There are no comments for now.