主流语言依赖注入对比

🚀 Java、Go、C++ 实践分析(以 log 和 db 为例)

在不同语言中,依赖注入的实现方式大相径庭:Java 倾向于使用框架,Go 倾向于显示传递,而 C++ 倾向于泛型或函数指针形式的注入。


🧱 场景设定

我们假设有一个 UserService 服务,它依赖一个日志组件(Logger)和一个数据库组件(DB)。我们希望通过依赖注入将这两个依赖提供给它,而不在 UserService 内部直接创建它们。


☕ Java:使用 Spring 框架的构造函数注入

Java 的依赖注入几乎都是通过框架(如 Spring)实现的,主流方式是构造器注入或注解注入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Component
public class UserService {
private final Logger logger;
private final Database db;

@Autowired
public UserService(Logger logger, Database db) {
this.logger = logger;
this.db = db;
}

public void createUser(String name) {
logger.log("Creating user: " + name);
db.saveUser(name);
}
}

配置方式(XML 或注解)交由 Spring 容器管理:

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
public class AppConfig {
@Bean
public Logger logger() {
return new ConsoleLogger();
}

@Bean
public Database database() {
return new MySQLDatabase();
}
}

优点

  • 框架自动管理依赖,易用且强大
  • 支持生命周期管理、作用域、AOP 等

⚠️ 缺点

  • 学习曲线较陡
  • 项目初始化成本高

🦫 Go:显示构造函数注入(无框架,清晰可控)

Go 倾向于使用显示注入(manual injection),遵循组合优于继承的思想。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
type Logger interface {
Log(msg string)
}

type DB interface {
SaveUser(name string)
}

type UserService struct {
logger Logger
db DB
}

func NewUserService(logger Logger, db DB) *UserService {
return &UserService{logger: logger, db: db}
}

func (u *UserService) CreateUser(name string) {
u.logger.Log("Creating user: " + name)
u.db.SaveUser(name)
}

创建时注入:

1
2
3
4
5
6
7
func main() {
logger := &ConsoleLogger{}
db := &MySQL{}
userService := NewUserService(logger, db)

userService.CreateUser("Alice")
}

优点

  • 无魔法,依赖清晰可见
  • 更易于测试和维护

⚠️ 缺点

  • 项目规模变大后需要手动组织构造器
  • DI 逻辑容易散乱

🛠️ Go 项目中常用 wire、fx、dig 等工具实现自动依赖注入,但本质还是构造函数注入。


🧠 C++:泛型 & 函数注入(模板 + std::function)

C++ 中没有内建 DI 框架,常用两种方式:

  1. 构造函数注入(经典方式)
  2. 函数式注入(适合注入策略或行为)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <iostream>
#include <functional>

class Logger {
public:
virtual void log(const std::string& msg) = 0;
};

class ConsoleLogger : public Logger {
public:
void log(const std::string& msg) override {
std::cout << "[LOG] " << msg << std::endl;
}
};

class DB {
public:
virtual void saveUser(const std::string& name) = 0;
};

class MySQL : public DB {
public:
void saveUser(const std::string& name) override {
std::cout << "Saving user: " << name << std::endl;
}
};

class UserService {
public:
UserService(Logger* logger, DB* db)
: logger_(logger), db_(db) {}

void createUser(const std::string& name) {
logger_->log("Creating user: " + name);
db_->saveUser(name);
}

private:
Logger* logger_;
DB* db_;
};

int main() {
ConsoleLogger logger;
MySQL db;

UserService service(&logger, &db);
service.createUser("Bob");
}

优点

  • 灵活、无外部依赖
  • 可轻松注入不同实现用于测试或扩展

⚠️ 缺点

  • 缺少统一管理机制
  • 生命周期管理需自行处理(裸指针 vs 智能指针)

🧾 总结对比表

特性 Java (Spring) Go C++
框架支持 强,自动注入 无/弱(dig、wire) 基本无
DI 类型 注解、XML、构造器注入 构造器注入 构造器注入 / 函数注入
生命周期管理 自动 手动 手动(注意指针)
学习成本 中高
测试友好性
推荐项目规模 中大型企业应用 中小型/微服务 嵌入式/性能敏感/底层系统

🏁 结语

依赖注入是一种提升代码灵活性、解耦性和测试能力的重要手段。不同语言有不同实现风格:

  • Java 倾向于框架驱动,自动注入、企业级开发常用;
  • Go 倾向于显示注入,符合其简洁哲学;
  • C++ 倾向于灵活注入,适合性能敏感场景;