Kratos promotes clean architecture through clear separation of concerns and dependency injection. While the framework doesn’t mandate a specific DI tool, it’s commonly used with Wire for compile-time dependency injection.
Core Concepts
Dependency injection in Kratos follows these principles:
Constructor Injection Dependencies are passed through constructor functions, making them explicit and testable.
Interface Contracts Components depend on interfaces rather than concrete types, enabling flexibility and testing.
Functional Options The framework uses the functional options pattern for configurable components.
No Global State Avoid global variables; pass dependencies explicitly through the dependency graph.
The Options Pattern
Kratos extensively uses the functional options pattern for configuration:
How It Works
The pattern is implemented using:
An unexported options struct holding configuration
An Option function type that modifies options
Constructor functions that accept variadic options
type Option func ( o * options )
type options struct {
id string
name string
version string
metadata map [ string ] string
endpoints [] * url . URL
ctx context . Context
sigs [] os . Signal
logger log . Logger
registrar registry . Registrar
registrarTimeout time . Duration
stopTimeout time . Duration
servers [] transport . Server
// Before and After funcs
beforeStart [] func ( context . Context ) error
beforeStop [] func ( context . Context ) error
afterStart [] func ( context . Context ) error
afterStop [] func ( context . Context ) error
}
Example Option Functions
// ID with service id.
func ID ( id string ) Option {
return func ( o * options ) { o . id = id }
}
// Name with service name.
func Name ( name string ) Option {
return func ( o * options ) { o . name = name }
}
// Server with transport servers.
func Server ( srv ... transport . Server ) Option {
return func ( o * options ) { o . servers = srv }
}
// Registrar with service registry.
func Registrar ( r registry . Registrar ) Option {
return func ( o * options ) { o . registrar = r }
}
Benefits
New options can be added without breaking existing code.
Options not provided use default values.
Each option clearly states what it configures.
Options can be stored, passed around, and composed.
Dependency Injection Without Wire
You can manually wire dependencies without any DI framework:
Basic Manual Wiring
Manual Dependency Injection
package main
import (
" github.com/go-kratos/kratos/v2 "
" github.com/go-kratos/kratos/v2/transport/http "
" github.com/go-kratos/kratos/v2/transport/grpc "
)
// Data layer
type UserRepo struct {
db * sql . DB
}
func NewUserRepo ( db * sql . DB ) * UserRepo {
return & UserRepo { db : db }
}
// Business layer
type UserService struct {
repo * UserRepo
}
func NewUserService ( repo * UserRepo ) * UserService {
return & UserService { repo : repo }
}
// API layer
type UserHandler struct {
service * UserService
}
func NewUserHandler ( service * UserService ) * UserHandler {
return & UserHandler { service : service }
}
// Wire it all together
func main () {
// Initialize dependencies
db := initDatabase ()
repo := NewUserRepo ( db )
service := NewUserService ( repo )
handler := NewUserHandler ( service )
// Create HTTP server with handler
httpSrv := http . NewServer (
http . Address ( ":8000" ),
)
registerHTTPHandlers ( httpSrv , handler )
// Create application
app := kratos . New (
kratos . Name ( "user-service" ),
kratos . Server ( httpSrv ),
)
if err := app . Run (); err != nil {
panic ( err )
}
}
Using Interfaces
Depend on interfaces for better testability:
// Define interfaces in business layer
type UserRepository interface {
GetUser ( ctx context . Context , id int ) ( * User , error )
SaveUser ( ctx context . Context , user * User ) error
}
// Business logic depends on interface
type UserService struct {
repo UserRepository // Interface, not concrete type
}
func NewUserService ( repo UserRepository ) * UserService {
return & UserService { repo : repo }
}
// Data layer implements interface
type MySQLUserRepo struct {
db * sql . DB
}
func NewMySQLUserRepo ( db * sql . DB ) UserRepository {
return & MySQLUserRepo { db : db }
}
func ( r * MySQLUserRepo ) GetUser ( ctx context . Context , id int ) ( * User , error ) {
// Implementation
}
func ( r * MySQLUserRepo ) SaveUser ( ctx context . Context , user * User ) error {
// Implementation
}
This makes testing easier with mock implementations:
type MockUserRepo struct {}
func ( m * MockUserRepo ) GetUser ( ctx context . Context , id int ) ( * User , error ) {
return & User { ID : id , Name : "Test User" }, nil
}
func ( m * MockUserRepo ) SaveUser ( ctx context . Context , user * User ) error {
return nil
}
func TestUserService ( t * testing . T ) {
mockRepo := & MockUserRepo {}
service := NewUserService ( mockRepo )
// Test service with mock repository
user , err := service . GetUser ( context . Background (), 1 )
assert . NoError ( t , err )
assert . Equal ( t , "Test User" , user . Name )
}
Using Wire for DI
Kratos projects typically use Google’s Wire for compile-time dependency injection:
Wire Basics
Wire uses code generation to create the wiring code at compile time:
//go:build wireinject
package main
import (
" github.com/google/wire "
)
// wireApp creates the application with all dependencies wired
func wireApp ( * conf . Config ) ( * kratos . App , func (), error ) {
panic ( wire . Build (
dataProvider ,
bizProvider ,
serviceProvider ,
serverProvider ,
newApp ,
))
}
Provider Sets
Group related providers into sets:
package data
import " github.com/google/wire "
// ProviderSet is data providers.
var ProviderSet = wire . NewSet (
NewDB ,
NewUserRepo ,
NewPostRepo ,
)
package biz
import " github.com/google/wire "
// ProviderSet is business providers.
var ProviderSet = wire . NewSet (
NewUserUseCase ,
NewPostUseCase ,
)
package service
import " github.com/google/wire "
// ProviderSet is service providers.
var ProviderSet = wire . NewSet (
NewUserService ,
NewPostService ,
)
Complete Wire Example
Define Providers
Create constructor functions for each component: // data/data.go
func NewDB ( cfg * conf . Data ) * sql . DB {
db , err := sql . Open ( "mysql" , cfg . Database . Source )
if err != nil {
panic ( err )
}
return db
}
func NewUserRepo ( db * sql . DB ) biz . UserRepo {
return & userRepo { db : db }
}
Create Provider Sets
Group providers by layer: // data/data.go
var ProviderSet = wire . NewSet (
NewDB ,
NewUserRepo ,
)
// biz/biz.go
var ProviderSet = wire . NewSet (
NewUserUseCase ,
)
Write Wire Injector
Define what you want Wire to build: //go:build wireinject
package main
import (
" github.com/google/wire "
" your-app/internal/biz "
" your-app/internal/data "
" your-app/internal/service "
" your-app/internal/server "
)
func wireApp ( * conf . Server , * conf . Data ) ( * kratos . App , func (), error ) {
panic ( wire . Build (
data . ProviderSet ,
biz . ProviderSet ,
service . ProviderSet ,
server . ProviderSet ,
newApp ,
))
}
Generate Wire Code
Run Wire to generate the wiring: This creates wire_gen.go with actual wiring code.
Use in Main
Call the generated function: func main () {
// Load config
cfg := loadConfig ()
// Wire creates everything
app , cleanup , err := wireApp ( & cfg . Server , & cfg . Data )
if err != nil {
panic ( err )
}
defer cleanup ()
// Run the application
if err := app . Run (); err != nil {
panic ( err )
}
}
Wire with Cleanup
Wire supports cleanup functions for resource disposal:
func NewDB ( cfg * conf . Data ) ( * sql . DB , func (), error ) {
db , err := sql . Open ( "mysql" , cfg . Database . Source )
if err != nil {
return nil , nil , err
}
cleanup := func () {
db . Close ()
}
return db , cleanup , nil
}
Wire automatically chains cleanup functions:
app , cleanup , err := wireApp ( cfg )
defer cleanup () // Calls all cleanup functions in reverse order
Server Registration Pattern
Kratos servers are registered using the options pattern:
// Create servers
func NewHTTPServer ( handler * service . UserService ) * http . Server {
srv := http . NewServer (
http . Address ( ":8000" ),
)
// Register handlers
v1 . RegisterUserServiceHTTPServer ( srv , handler )
return srv
}
func NewGRPCServer ( handler * service . UserService ) * grpc . Server {
srv := grpc . NewServer (
grpc . Address ( ":9000" ),
)
// Register services
v1 . RegisterUserServiceServer ( srv , handler )
return srv
}
// Provide to Wire
var ProviderSet = wire . NewSet (
NewHTTPServer ,
NewGRPCServer ,
)
Pass servers to the App:
func newApp ( hs * http . Server , gs * grpc . Server ) * kratos . App {
return kratos . New (
kratos . Name ( "user-service" ),
kratos . Server ( hs , gs ),
)
}
Layered Architecture with DI
Kratos encourages a layered architecture:
Dependency Rules
The Dependency Rule : Dependencies point inward. Outer layers depend on inner layers, never the reverse.
Transport layer depends on Service layer
Service layer depends on Business layer
Business layer depends on Data interfaces (but not implementations)
Data layer implements Business layer interfaces
Example Layer Structure
Business Layer (biz)
Data Layer (data)
Service Layer (service)
// Define interfaces
type UserRepo interface {
GetUser ( context . Context , int64 ) ( * User , error )
}
// Business logic
type UserUseCase struct {
repo UserRepo
}
func NewUserUseCase ( repo UserRepo ) * UserUseCase {
return & UserUseCase { repo : repo }
}
Best Practices
Constructors should only assign dependencies. Complex initialization logic should be in separate Init methods or lifecycle hooks.
Constructor functions should return interfaces when possible to reduce coupling.
Avoid circular dependencies
If you have circular dependencies, your architecture needs refactoring. Consider introducing an interface or intermediate layer.
Use lifecycle hooks for initialization
Instead of doing work in constructors, use BeforeStart hooks for complex initialization: app := kratos . New (
kratos . BeforeStart ( func ( ctx context . Context ) error {
return cache . Warmup ( ctx )
}),
)
Make dependencies explicit
Avoid hidden dependencies (like reading environment variables in constructors). Pass all configuration through parameters.
Testing with DI
Dependency injection makes testing straightforward:
func TestUserUseCase ( t * testing . T ) {
// Create mock repository
mockRepo := & MockUserRepo {
users : map [ int64 ] * biz . User {
1 : { ID : 1 , Name : "Test User" },
},
}
// Inject mock into use case
uc := biz . NewUserUseCase ( mockRepo )
// Test business logic
user , err := uc . GetUser ( context . Background (), 1 )
assert . NoError ( t , err )
assert . Equal ( t , "Test User" , user . Name )
}
Use interface-based design to make your components testable without needing the actual infrastructure.
Next Steps
Architecture Overview Learn about Kratos framework architecture
Application Lifecycle Understand the App lifecycle management