🚀 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 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++ 倾向于灵活注入,适合性能敏感场景;