The glued-together problem
You write a function to process orders. Inside, it creates an email client to send confirmations. It works. Then your boss says, "We need to support SMS too," and "Write tests that don't hit the real email server." You realize the function is glued to the email client. You can't swap it out. You can't mock it. The code is tight, and you're stuck.
This happens when a component manufactures its own dependencies. The function knows exactly how to build the email client, so it does. That knowledge leaks into the logic. The function now cares about two things: processing orders and configuring email. If the email library changes, you have to rewrite the order processor. If you want to test the order logic, you have to fake the email library from the inside, which is messy.
Dependency injection solves this by flipping the direction. Instead of the function creating the dependency, the dependency is passed in from the outside. The function stops caring about how the email client is built. It only cares that something exists which can send messages.
Dependency injection is just passing arguments
In Go, dependency injection is not a framework. It is not a library. It is a design pattern implemented with the language's built-in features. The core idea is simple: pass what you need as an argument to a constructor or a function.
Think of a carpenter. A carpenter doesn't grow a hammer out of their hand. They pick up a hammer. If the hammer breaks, they grab a new one. The carpenter's skill isn't tied to that specific hammer. The carpenter depends on the hammer, but the carpenter doesn't manufacture it.
In code, a dependency is anything a function or struct needs to do its job. A database connection, a logger, an HTTP client, a configuration value. Injection means you provide that dependency when you create the object, rather than letting the object create it itself.
Go makes this pattern natural because of interfaces. You define an interface that describes the behavior you need. You pass that interface into your constructor. The caller provides the concrete implementation. Your code works with the interface, so it doesn't know or care about the concrete type.
The minimal pattern
Here's the simplest form of dependency injection: a struct holds an interface, and a constructor function accepts that interface as an argument.
// Service defines the behavior the worker needs.
// Using an interface decouples the worker from any specific implementation.
type Service interface {
Process() error
}
// Worker depends on a Service but doesn't know what kind it is.
// The field is private because the worker manages it internally.
type Worker struct {
svc Service
}
// NewWorker creates a Worker and requires a Service to be provided.
// The caller decides which concrete type satisfies the Service interface.
func NewWorker(svc Service) *Worker {
return &Worker{svc: svc}
}
The Worker struct has a field svc of type Service. That field is an interface. Any type that has a Process() error method satisfies this interface. Go interfaces are satisfied implicitly. You don't need to declare implements. The compiler checks the method set.
When you call NewWorker, you pass a concrete struct. The compiler verifies that the struct has the required methods. If you pass a type that doesn't match, the build fails with cannot use realService (type *RealService) as Service value in argument: *RealService does not implement Service (missing Process method). This error catches mistakes early.
At runtime, the Worker calls svc.Process(). The interface value holds a pointer to the concrete type and a method table. The call dispatches to the correct implementation. The Worker never sees the concrete type. It only sees the contract.
Interfaces are contracts. Structs are implementations. Keep them separate.
Wiring it up in real code
In a real application, you have multiple dependencies. An HTTP handler might need a database store and a logger. The handler shouldn't create the database or the logger. It should accept them.
Here's a realistic example: an HTTP handler that retrieves items from a store.
// Store defines database operations.
// The interface is small, focusing only on what the handler needs.
type Store interface {
GetItem(id string) (Item, error)
}
// Item represents a domain entity.
type Item struct {
ID string
Name string
}
// Handler processes HTTP requests.
// It accepts dependencies via the constructor, keeping the handler focused on HTTP logic.
type Handler struct {
store Store
}
// NewHandler wires up the handler with its dependencies.
// This is where the concrete implementations are chosen.
func NewHandler(store Store) *Handler {
return &Handler{store: store}
}
// ServeHTTP handles the request using the injected store.
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
id := r.URL.Query().Get("id")
if id == "" {
http.Error(w, "missing id", http.StatusBadRequest)
return
}
// Call the injected store to fetch data.
item, err := h.store.GetItem(id)
if err != nil {
http.Error(w, "not found", http.StatusNotFound)
return
}
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, "%s", item.Name)
}
The Handler struct holds a Store interface. The constructor NewHandler accepts the store. The ServeHTTP method uses the store to get data. The handler doesn't know if the store is PostgreSQL, Redis, or an in-memory map. It only knows the store has a GetItem method.
The wiring happens at the top level, usually in main or a setup function. This is where you create the concrete types and pass them down.
func main() {
// Create the real database implementation.
// This is the only place that knows about Postgres.
db := NewPostgresStore()
// Inject the database into the handler.
// The handler receives an interface, not the concrete type.
handler := NewHandler(db)
// Start the server.
http.ListenAndServe(":8080", handler)
}
The main function is the composition root. It decides which implementations to use. The rest of the code depends on abstractions. This keeps the concrete details at the edges of the program.
Wire it up at the top. Let the edges decide.
Testing becomes trivial
The main benefit of dependency injection is testability. When dependencies are injected, you can swap them for mocks. A mock is a fake implementation that records calls or returns controlled data.
Here's a mock for the Store interface.
// MockStore implements Store for testing.
// It records calls instead of doing real work.
type MockStore struct {
items map[string]Item
}
// GetItem returns an item from the map, simulating database behavior.
func (m *MockStore) GetItem(id string) (Item, error) {
item, ok := m.items[id]
if !ok {
return Item{}, fmt.Errorf("item not found")
}
return item, nil
}
Now you can write a test that uses the mock. The test doesn't need a real database. It doesn't need network access. It runs fast and deterministically.
func TestHandler_GetItem(t *testing.T) {
// Create a mock store with test data.
mock := &MockStore{
items: map[string]Item{
"123": {ID: "123", Name: "Widget"},
},
}
// Inject the mock into the handler.
handler := NewHandler(mock)
// Create a test request.
req := httptest.NewRequest(http.MethodGet, "/?id=123", nil)
w := httptest.NewRecorder()
// Call the handler.
handler.ServeHTTP(w, req)
// Assert the result.
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d", w.Code)
}
if w.Body.String() != "Widget" {
t.Errorf("expected body 'Widget', got %q", w.Body.String())
}
}
The test creates a MockStore, injects it into the handler, and verifies the behavior. If the handler logic changes, the test catches it. If the mock changes, the test catches it. The handler code doesn't change between production and testing.
If you can't mock it, you can't test it. DI makes mocking easy.
Why Go doesn't need a framework
Developers coming from Java or C# often expect a dependency injection framework. Tools like Spring or Autofac scan your code, create objects, and wire them together automatically. Go doesn't work that way. Go favors explicit code over hidden magic.
Go has everything you need for DI built in. Constructors are just functions. Interfaces are implicit. You don't need a framework to pass arguments. A framework adds complexity, runtime overhead, and a learning curve. It also hides the wiring, making it harder to see how components connect.
In Go, the wiring is explicit. You read NewHandler(db) and you know the handler depends on the database. You don't need to search for annotations or configuration files. The code tells you the truth.
This approach aligns with the Go mantra: "Accept interfaces, return structs." Functions and constructors accept interfaces, so they work with abstractions. They return structs, so the caller gets a concrete type. This keeps dependencies flowing in one direction. The caller provides the implementation. The callee provides the contract.
Don't over-engineer. Go's simplicity is a feature.
Pitfalls and conventions
Dependency injection is simple, but there are common mistakes.
Interface bloat is a frequent issue. Some developers define an interface for every single type. This creates unnecessary abstraction. If a type is only used in one place, you don't need an interface. Define interfaces where they are used, not where they are implemented. Wait until you have a reason to abstract. A common rule is to define an interface when you need a second implementation or when you need to mock for testing.
Circular dependencies are another pitfall. If package A depends on package B, and package B depends on package A, you have a cycle. Go compilers reject this with import cycle not allowed. Dependency injection can help break cycles by moving interfaces to a separate package, but it's better to design your architecture to avoid cycles in the first place. Keep dependencies flowing downward.
Constructor validation is a good practice. If a dependency is required, check it in the constructor.
func NewWorker(svc Service) *Worker {
if svc == nil {
panic("Worker: svc is required")
}
return &Worker{svc: svc}
}
This fails fast. If someone forgets to pass the dependency, the program panics immediately with a clear message. It's better to panic at startup than to crash later with a nil pointer dereference.
Follow Go conventions. Receiver names should be short, usually one or two letters matching the type. Use (h *Handler), not (this *Handler). Constructor functions start with New and return a pointer to the struct. Use NewWorker, not CreateWorker or worker. The community expects this pattern.
Also, context.Context is often a dependency. Functions that perform I/O should accept a context as the first parameter. The context carries deadlines, cancellation signals, and request-scoped values. Pass the context through your dependency chain. If your service needs a context, the handler should pass it down.
Context is plumbing. Run it through every long-lived call site.
When to use dependency injection
Dependency injection is a tool. Use it when it solves a problem. Don't use it everywhere.
Use dependency injection when a component needs to work with multiple implementations, such as swapping a real database for a mock during tests.
Use dependency injection when you want to keep the construction of complex objects separate from their usage, making the code easier to read and maintain.
Use dependency injection when a dependency has expensive setup or lifecycle management, so you can control its creation and cleanup at the composition root.
Use direct instantiation when the dependency is trivial and never needs to change, such as a simple utility function with no external side effects.
Use a constructor function when you need to validate dependencies or perform setup before the object is usable.
Use struct fields for optional dependencies that have sensible defaults, reserving constructor arguments for required inputs.
Start simple. Add interfaces when you need them.