注册

一个 Java 老兵转 Go 后,终于理解了“简单”的力量

之前写的文章《信不信?一天让你从Java工程师变成Go开发者》很受关注,很多读者对 Go 的学习很感兴趣。今天就再写一篇,聊聊 Java 程序员写 Go 时最常见的思维误区。


核心观点: Go 不需要 Spring 式的依赖注入框架,因为它的设计哲学是"显式优于隐式"。手动构造依赖看似啰嗦,实则更清晰、更快、更易调试。


从 Java 转 Go,第一天就会被这个问题困扰:"@Autowired 在哪?依赖注入框架用哪个?IoC 容器怎么配?"


答案很直接:Go 里没有,也不需要。 不是 Go 做不到,而是 Go 压根不想这么干。这不是功能缺失,而是设计哲学的根本性差异。




第一反应:Go 怎么这么"原始"?


刚开始写 Go,看到的代码是这样的:


func main() {
// 手动创建数据库连接
db := NewDB("localhost:3306", "user", "password")

// 手动创建各种 Service
userRepo := NewUserRepository(db)
orderRepo := NewOrderRepository(db)

paymentSvc := NewPaymentService(db)
inventorySvc := NewInventoryService(db)

orderSvc := NewOrderService(
orderRepo,
paymentSvc,
inventorySvc,
)

userSvc := NewUserService(userRepo)

// 手动创建 HTTP Handler
handler := NewHandler(orderSvc, userSvc)

// 启动服务
http.ListenAndServe(":8080", handler)
}

第一反应:这种写法让人想起早期的 Java 或 PHP


在 Java 里,这些全是框架干的事:


@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
// 就这一行,所有对象都帮你创建好了
}
}

@RestController
public class OrderController {
@Autowired
private OrderService orderService;

// 框架自动注入,你根本看不到对象怎么创建的
}

Java 开发者心里 OS:



  • "Go 是不是太简陋了?"
  • "难道要我手动 new 几十个对象?"
  • "这不是倒退吗?"

先别急着下结论,听我说完。




为什么 Go 要这么"原始"?


Go 的设计哲学就一句话:



显式优于隐式,简单优于复杂。



这不是口号,而是实实在在的取舍。


对比1:依赖是怎么传递的?


Java/Spring 的做法:


// 你写这个
@Service
public class OrderService {
@Autowired
private PaymentService paymentService;

@Autowired
private InventoryService inventoryService;

@Autowired
private NotificationService notificationService;
}

// 框架在背后做了:
// 1. 扫描所有类
// 2. 分析依赖关系
// 3. 构建依赖图
// 4. 按顺序创建对象
// 5. 通过反射注入字段
// 6. 处理循环依赖
// 7. 管理生命周期

这些魔法看起来很方便,但:



  • 你不知道对象什么时候创建的
  • 你不知道注入顺序是什么
  • 出问题了,调试要靠猜
  • 启动慢(要扫描、要反射)
  • 内存大(要维护容器)

Go 的做法:


type OrderService struct {
paymentSvc *PaymentService
inventorySvc *InventoryService
notificationSvc *NotificationService
}

func NewOrderService(
paymentSvc *PaymentService,
inventorySvc *InventoryService,
notificationSvc *NotificationService,
)
*OrderService {
return &OrderService{
paymentSvc: paymentSvc,
inventorySvc: inventorySvc,
notificationSvc: notificationSvc,
}
}

// 在 main 里
paymentSvc := NewPaymentService(db)
inventorySvc := NewInventoryService(db)
notificationSvc := NewNotificationService(queue)

orderSvc := NewOrderService(
paymentSvc,
inventorySvc,
notificationSvc,
)

这些代码看起来很啰嗦,但:



  • 你清楚地看到每个对象怎么创建的
  • 你清楚地看到依赖关系是什么
  • 出问题了,一眼就能定位
  • 启动快(没有扫描、没有反射)
  • 内存小(没有容器)

对比2:遇到问题怎么调试?


Java/Spring 遇到问题:


报错:Could not autowire. No beans of 'PaymentService' type found.

你要做的:
1. 检查 PaymentService 有没有 @Service
2. 检查包扫描路径对不对
3. 检查有没有循环依赖
4. 检查 @Conditional 条件是否满足
5. 检查配置文件有没有禁用
6. Google 半天
7. 还不行,看 Spring 源码

根本原因可能是:配置文件里有个 typo

Go 遇到问题:


编译报错:undefined: paymentSvc

你要做的:
1. 看报错的那一行
2. 发现没有传 paymentSvc 参数
3. 改完,搞定

5 秒钟解决

对比3:新人上手难度


Java/Spring 新人:


"这个对象哪来的?"
"@Autowired@Resource 有什么区别?"
"为什么我的 Bean 没有注入?"
"循环依赖怎么解决?"
"什么是 BeanPostProcessor?"

要学的概念:
- IoC 容器
- 依赖注入
- Bean 生命周期
- AOP
- 代理模式
- ...

Go 新人:


"这个对象哪来的?"
"看 main 函数,就是在那 New 出来的。"
"哦,明白了。"

要学的概念:
- 函数
- 指针



Go 为什么说自己"像脚本语言"?


Go 的设计目标就是:



写起来像脚本语言一样简单,跑起来像编译型语言一样快。



什么叫"像脚本语言"?


PHP 的写法:


<?php
// 直接开始写逻辑
$db = new PDO('mysql:host=localhost', 'user', 'pass');
$userRepo = new UserRepository($db);
$user = $userRepo->find(1);
echo $user->name;

Python 的写法:


# 直接开始写逻辑
db = connect_db('localhost', 'user', 'pass')
user_repo = UserRepository(db)
user = user_repo.find(1)
print(user.name)

Go 的写法:


func main() {
// 直接开始写逻辑
db := NewDB("localhost", "user", "pass")
userRepo := NewUserRepository(db)
user := userRepo.Find(1)
fmt.Println(user.Name)
}

看出来了吗?Go 就是想让你像写脚本一样写代码。


不需要:



  • 复杂的配置文件
  • 注解魔法
  • 框架黑盒
  • 反射黑魔法

只需要:



  • 创建对象
  • 调用方法
  • 传递参数

但是,它不是脚本语言:



  • 有强类型检查(写错了编译不过)
  • 编译成二进制(部署一个文件)
  • 性能接近 C(比 Java 快很多)
  • 启动秒开(没有 JVM 预热)



这种差异带来的实际影响


理论说完了,看看实际项目中的差异。


场景1:启动速度


Java/Spring 项目:


启动流程:
1. JVM 启动(1-2秒)
2. 加载类(2-3秒)
3. 扫描注解(3-5秒)
4. 构建依赖图(2-3秒)
5. 初始化 Bean(5-10秒)
6. AOP 代理(2-3秒)

总计:15-30秒

项目大了:1-2分钟

Go 项目:


启动流程:
1. 执行 main 函数
2. 创建对象
3. 启动服务

总计:0.1-0.5秒

项目再大:也就几秒

这就是为什么 Go 适合做 CLI 工具、K8s 组件:启动快


场景2:内存占用


Java/Spring 项目:


启动后内存:
- JVM 基础:100-200MB
- Spring 容器:50-100MB
- 对象缓存:100-200MB

最小内存:300-500MB
实际运行:1-2GB

Go 项目:


启动后内存:
- 没有虚拟机
- 没有容器
- 只有你创建的对象

最小内存:10-20MB
实际运行:50-200MB

这就是为什么 Go 适合做微服务、容器应用:省资源


场景3:调试体验


Java/Spring 遇到空指针:


// 报错
NullPointerException at OrderService.process()

// 原因可能是:
1. paymentService 没有注入成功
2. 某个 @Conditional 条件不满足
3. 循环依赖导致代理失败
4. 配置文件写错了

// 排查过程:
- 看日志,找不到原因
- 打断点,发现字段是 null
- Google,找到类似问题
- 尝试各种方案
- 1小时后,发现是配置文件拼写错误

Go 遇到空指针:


// 报错
panic: runtime error: invalid memory address

// 看代码
orderSvc := NewOrderService(
paymentSvc,
nil, // 这里忘了传
notificationSvc,
)

// 排查过程:
- 看报错行号
- 看代码
- 发现 nil
- 改完,搞定

// 5 秒钟解决



Java 开发者常犯的错误


看几个 Java 开发者写 Go 时常犯的错误。


错误1:找依赖注入框架


错误想法:
"Go 的依赖注入框架哪个好?Wire?Dig?"

正确做法:
别找了,手动传参就够了

有些 Go 项目确实用了 Wire、Dig,但那是因为:



  • 项目太大(100+ 个 Service)
  • 自动生成代码,减少重复

大部分项目,手动传参就够了。


错误2:过度抽象


// 错误做法:照搬 Java 那套
type ServiceFactory interface {
CreateUserService() UserService
CreateOrderService() OrderService
}

type ServiceFactoryImpl struct {
db *DB
}

func (f *ServiceFactoryImpl) CreateUserService() UserService {
return NewUserService(f.db)
}

// 正确做法:直接创建
func main() {
db := NewDB()
userSvc := NewUserService(db)
orderSvc := NewOrderService(db)
}

错误3:到处用接口


// 错误做法:每个 struct 都配个 interface
type UserService interface {
GetUser(id int) (*User, error)
}

type UserServiceImpl struct {
repo *UserRepository
}

// 正确做法:需要 mock 时才定义 interface
type UserService struct {
repo *UserRepository
}

// 测试时才定义
type UserRepository interface {
Find(id int) (*User, error)
}

Go 的接口是隐式实现的,不需要到处声明。


错误4:配置文件过度使用


# 错误做法:把所有配置都写 YAML
database:
host: localhost
port: 3306
user: root

services:
user:
enabled: true
timeout: 5s
order:
enabled: true
timeout: 10s

// 正确做法:代码即配置
func main() {
db := NewDB("localhost:3306", "root", "password")

userSvc := NewUserService(db, 5*time.Second)
orderSvc := NewOrderService(db, 10*time.Second)
}

Go 的理念是:代码就是最好的配置




什么时候该用依赖注入框架?


话说回来,真的完全不需要 DI 框架吗?也不是。


适合手动传参的场景(大部分情况)


小型项目(<50 个组件)


// 清晰、直接、易调试
func main() {
db := NewDB()
cache := NewCache()

userRepo := NewUserRepository(db)
orderRepo := NewOrderRepository(db)

userSvc := NewUserService(userRepo, cache)
orderSvc := NewOrderService(orderRepo, userSvc)

// 10-20 个组件,完全可控
}

中型项目(50-100 个组件)


// 可以考虑分组管理
type Services struct {
User *UserService
Order *OrderService
// ...
}

func InitServices(db *DB) *Services {
userRepo := NewUserRepository(db)
orderRepo := NewOrderRepository(db)

return &Services{
User: NewUserService(userRepo),
Order: NewOrderService(orderRepo),
}
}

适合用 DI 框架的场景


大型微服务(>100 个组件)


当你的项目有 100+ 个 Service、Repository、Client 时,手动传参确实会很繁琐。这时可以考虑:


Wire(Google 官方推荐)



  • 编译时生成代码,不是运行时反射
  • 性能无损耗
  • 类型安全
  • 适合大型项目

// wire.go
//go:build wireinject

func InitializeApp() (*App, error) {
wire.Build(
NewDB,
NewUserRepository,
NewUserService,
NewApp,
)
return nil, nil
}

// wire 会自动生成代码

Dig(Uber 出品)



  • 运行时依赖注入
  • 更灵活,但有性能开销
  • 适合需要动态配置的场景

判断标准:


组件数 < 50 个      → 手动传参
组件数 50-100 个 → 手动传参 + 分组管理
组件数 > 100 个 → 考虑 Wire
需要插件化/动态加载 → 考虑 Dig

CLI 工具/脚本类应用 → 绝对不需要 DI 框架

记住一个原则:

不要为了"看起来像企业级架构"而引入 DI 框架。大部分 Go 项目,手动传参就够了。




澄清一个误解:Go 不是"反对抽象"


看到这里,有些人可能会想:"Go 这么简单粗暴,是不是就是写面条代码?"


不是的。


Go 的设计哲学不是"反对抽象",而是**"反对过早抽象、反对过度抽象"**。


Go 鼓励的抽象方式


1. 需要解耦时才引入接口


// 错误做法:提前抽象
type UserService interface {
GetUser(id int) (*User, error)
CreateUser(user *User) error
}

type UserServiceImpl struct { }

// 正确做法:需要 mock 时才抽象
type UserService struct {
repo UserRepository // 这里才用接口
}

type UserRepository interface {
Find(id int) (*User, error)
Save(user *User) error
}

2. 真正需要多态时才用接口


// 有多个实现时才抽象
type Storage interface {
Save(key string, value []byte) error
Load(key string) ([]byte, error)
}

// 文件存储实现
type FileStorage struct { }

// Redis 存储实现
type RedisStorage struct { }

// S3 存储实现
type S3Storage struct { }

Go 的理念是:



  • 先用具体类型写代码
  • 发现真正需要抽象时(测试、多实现),再引入接口
  • 不要为了"看起来专业"而提前抽象

这不是反对抽象,而是在正确的时机做正确的事。




什么时候该用框架?


话说回来,难道 Go 就完全不需要框架了?


也不是。


适合用框架的场景


1. HTTP 路由:Gin、Echo


// 标准库的 http.ServeMux 太简陋
// 用 Gin 处理路由、中间件更方便

r := gin.Default()
r.GET("/users/:id", getUser)
r.POST("/orders", createOrder)

2. ORM:GORM


// 标准库的 database/sql 写 SQL 太麻烦
// 用 GORM 处理关联查询更方便

db.Where("age > ?", 18).Find(&users)

3. 配置管理:Viper


// 管理多环境配置
viper.SetConfigName("config")
viper.ReadInConfig()

不适合用框架的场景


1. 依赖注入


不需要 Wire、Dig,手动传参就够了。


2. 业务逻辑


不要用框架包装业务逻辑,直接写代码。


3. 简单功能


不要为了"看起来专业"而引入框架。




给 Java 开发者的建议


如果你是 Java 开发者,开始写 Go,记住这几点:


1. 忘掉 Spring 那套


别想着:
- 在哪配置注解
- 怎么注入依赖
- 怎么用 AOP

直接写代码就行

2. 拥抱"啰嗦"


Java 开发者看 Go:
"怎么要手动 new 这么多对象?太啰嗦了!"

写一段时间后:
"原来清晰明了比简洁更重要。"

3. 代码即文档


Java 项目:
- 要看 XML 配置
- 要看注解定义
- 要看框架文档

Go 项目:
- 看 main 函数
- 看 NewXXX 函数
- 看代码就够了

4. 简单优于复杂


遇到问题:
第一反应不是"找个框架"
而是"能不能写100行代码搞定"

90%的情况,100行代码就够了



一个实际例子


最后用一个例子,对比一下两种风格。


场景:订单服务


需要:



  • 数据库操作
  • 支付服务调用
  • 库存服务调用
  • 通知服务调用

Java/Spring 实现


// Application.java
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}

// OrderController.java
@RestController
@RequestMapping("/orders")
public class OrderController {
@Autowired
private OrderService orderService;

@PostMapping
public Order create(@RequestBody CreateOrderRequest req) {
return orderService.create(req);
}
}

// OrderService.java
@Service
public class OrderService {
@Autowired
private OrderRepository orderRepository;

@Autowired
private PaymentService paymentService;

@Autowired
private InventoryService inventoryService;

@Autowired
private NotificationService notificationService;

public Order create(CreateOrderRequest req) {
// 业务逻辑
}
}

配置文件 application.yml:


spring:
datasource:
url: jdbc:mysql://localhost:3306/db
username: root
password: password

代码文件: 4个

配置文件: 1个

看起来: 很简洁

实际运行: 一堆魔法


Go 实现


// main.go
func main() {
// 创建依赖
db := NewDB("localhost:3306", "root", "password")
defer db.Close()

orderRepo := NewOrderRepository(db)
paymentSvc := NewPaymentService()
inventorySvc := NewInventoryService()
notificationSvc := NewNotificationService()

orderSvc := NewOrderService(
orderRepo,
paymentSvc,
inventorySvc,
notificationSvc,
)

// 创建 HTTP Handler
r := gin.Default()
r.POST("/orders", func(c *gin.Context) {
var req CreateOrderRequest
if err := c.BindJSON(&req); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}

order, err := orderSvc.Create(req)
if err != nil {
c.JSON(500, gin.H{"error": err.Error()})
return
}

c.JSON(200, order)
})

// 启动服务
r.Run(":8080")
}

// order_service.go
type OrderService struct {
orderRepo *OrderRepository
paymentSvc *PaymentService
inventorySvc *InventoryService
notificationSvc *NotificationService
}

func NewOrderService(
orderRepo *OrderRepository,
paymentSvc *PaymentService,
inventorySvc *InventoryService,
notificationSvc *NotificationService,
)
*OrderService {
return &OrderService{
orderRepo: orderRepo,
paymentSvc: paymentSvc,
inventorySvc: inventorySvc,
notificationSvc: notificationSvc,
}
}

func (s *OrderService) Create(req CreateOrderRequest) (*Order, error) {
// 业务逻辑
}

代码文件: 2个

配置文件: 0个

看起来: 有点啰嗦

实际运行: 一目了然




最后的思考:从 Spring 到 main(),是一次思维升级


从 Java 转 Go,最大的障碍不是语法,而是思维方式。


Java/Spring 的思维:



  • 框架帮你管理一切
  • 抽象层次越高越好
  • 配置优于代码
  • "我不需要知道对象怎么创建的,框架会处理"

Go 的思维:



  • 你自己管理一切
  • 简单直接就够了
  • 代码即配置
  • "我清楚地知道每个对象是怎么来的"

这不是谁对谁错,而是不同的设计哲学,适合不同的场景。


给 Java 开发者的建议


如果你从 Java 转 Go,记住这几点:


1. 拥抱"啰嗦",它带来的是清晰


刚开始:
"怎么要手动 new 这么多对象?太麻烦了!"

一个月后:
"原来看一眼 main 函数就知道整个系统是怎么组装的。"

2. 别急着找"Go 的 Spring"


Go 生态里有很多框架,但:
- 不要为了"看起来专业"而引入框架
- 不要为了"企业级架构"而过度设计
- 先写代码解决问题,再考虑是否需要框架

3. 代码即文档


Java 项目理解成本:
- 看配置文件
- 看注解定义
- 看框架文档
- 猜测对象是怎么创建的

Go 项目理解成本:
- 看 main 函数
- 看 NewXXX 函数
- 就这么简单

4. 简单优于复杂


遇到问题时:
第一反应不是"有没有框架能解决"
而是"能不能写 100 行代码搞定"

90% 的情况,100 行代码就够了

从 Spring 到 main(),不是倒退,而是升级


你失去的是:



  • 自动注入的"魔法"
  • 复杂的抽象层次
  • 庞大的框架依赖

你获得的是:



  • 对系统的完全掌控
  • 清晰可见的执行流程
  • 快速的启动和调试
  • 简单直接的代码组织

这不是倒退,而是一次返璞归真的旅程


最后的鼓励


从 Spring 的"魔法"转到 Go 的"手工",一开始可能会不适应。


你可能会觉得:



  • "怎么这么原始?"
  • "怎么要写这么多重复代码?"
  • "没有框架怎么办?"

但坚持一周,你会发现:



  • 代码更清晰了
  • 调试更简单了
  • 启动更快了
  • 部署更轻了

再过一个月,当你回头看 Spring 项目时,你会想:



  • "这个对象是怎么创建的?"
  • "这个注解背后做了什么?"
  • "为什么启动要 30 秒?"

那时候,你就真正理解了 Go 的设计哲学。


记住:



Go 的哲学是:显式优于隐式,简单优于复杂。

从 Spring 到 main(),你失去的是魔法,获得的是掌控。



适应这个哲学,你就适应了 Go。


欢迎来到 Go 的世界。


就这样。


作者:踏浪无痕
来源:juejin.cn/post/7587712328826224676

0 个评论

要回复文章请先登录注册