
这个话题被写过太多次了,我原本不打算再写。但每次有人问我"新项目用什么语言",最后绕回来的还是那几个老问题:内存够不够、启动要多久、发一次版要等多久、出了故障好不好查。
微服务正好把这几个问题放大。单体应用里进程多吃 50 兆,你可能一年都不知道;拆成八个服务、每个跑三副本,同样的浪费就变成二十四份。
所以这篇不聊语法偏好,只聊账单和日常手感。
先把范围划清楚:这里说的"适合",指的是以网络接口为主、需要频繁交付和独立扩容的服务。你要是做模型训练、写桌面端,或者团队里全是 Java 老兵,下面的结论不一定适用。
单体时代你不太会在意基础开销。拆开之后,每个服务都要有自己的连接池、配置加载、健康检查、监控上报——这些东西跟业务没关系,但每个副本都得付一遍。
Go 在这里的好处很直接:它是提前编译的,运行时跟程序一起打进去,机器上不需要另外装一套 JDK,也没有 JIT 一边跑一边优化热点的过程。
但别把这句话听成"Go 没有运行时开销"。Go 有自己的运行时和 GC:goroutine 的调度、栈的伸缩、垃圾回收的标记扫描,都要花 CPU 和时间。(来源:Go 官方技术文章)
真正的差距不是"有没有",而是"多少",而多少只能拿自己的服务测。同一个接口、同样的 QPS、同样的监控口径,跑一遍才有数字。
单看一个实例省下的那点资源,说实话没什么意思。乘上实例数,再乘上一年 365 天,它才会出现在服务器账单上。
写业务代码的日常是这样的:改一行、构建、跑一下、再改一行。
这个循环越短,你越容易记住自己刚才在想什么。一旦构建要等两分钟,人就会去刷会儿手机,回来思路断了,得重新捡一遍上下文。这才是编译慢真正的成本,它偷的不是机器的秒数,是你的注意力。
Go 在这方面下了功夫。编译器本身就是工具链的一部分,不用另外配;构建结果会缓存,没变的部分直接复用。(来源:Go FAQ、Go 命令文档)
界限也得说清楚:第一次拉依赖、超大仓库,或者工程里有 cgo 的部分,该慢还是慢。别指望所有项目都能秒出结果。
比起纠结两个编译器谁更快,我更在意"从改完代码到拿到可验证结果"这条路径有多长。Go 这条路通常比较短,而且不太需要你动脑子去想怎么配——对一天要在几个服务之间切来切去的团队来说,这比峰值性能值钱。
微服务不是启动一次就不动了。滚动发布、实例挂了要补上、流量上来临时扩容,每次都是新进程冷启动。
想一个具体场景:晚上八点流量抬头,调度平台决定把副本从 6 个扩到 12 个。这 6 个新副本被调度、拉起、初始化、通过健康检查,中间那几十秒到几分钟里,你的实际承载能力还是 6 个。扩容决定只是一种意图,不是容量。
Go 的提前编译模式,加上可以写得很简洁的初始化流程,能让新实例比较快地进入工作状态。
要分清的是:进程起来了不等于服务就绪。连数据库、拉配置、预热缓存、等健康检查,这些环节一样能拖住你。用 Go 只是减少了语言本身的启动负担。特别是那种在 main 里同步调好几个下游接口、调通了才开始监听端口的写法,换什么语言都救不了。
微服务里大部分时间花在等:等数据库返回、等缓存、等另一个服务出结果。这类活儿本质上是并发编排。
goroutine 处理这类任务很顺手。64 位平台上它的起始栈只有 2KB,由运行时调度到一组 OS 线程上,你不用为每个并发任务单独开一个系统线程,写起来就是 go 加一个 channel。(来源:Effective Go)
这里有个坑我见过太多次:把 go 当成免费的。goroutine 是很轻,但它不会自己消失,在没超时、没取消、没上限的循环里一直起,内存照样涨到 OOM。写并发真正花时间的地方是超时设多少、上下文怎么取消、并发上限设几、下游扛不住时怎么背压。这些跟语言没关系,是设计问题。
还有一句:拿"Java 一个请求占一个昂贵的平台线程"来证明 Go 更好,现在已经站不住了。JDK 21 的虚拟线程(JEP 444)已经正式落地,轻量并发不再是 Go 独有的卖点。Go 的优势在于它把这件事做进了语言和日常写法里,不需要你特意去用某个新特性。
我很喜欢 Go 的一个工程细节:对纯 Go 的服务来说,构建产物主要就是一个可执行文件。目标机器上不用装 Go,不用装 JDK,不用配 JAVA_HOME,不用管 classpath。
只要操作系统和架构对得上,发版就是把文件传过去、换成新版本、重启。镜像能做得很小,回滚就是把上一个文件换回来。
注意这里说的是"纯 Go"。用到 cgo、动态库或者外部资源的时候,依赖该处理还是得处理。(来源:Go FAQ)
没有专职运维的团队最能体会这个差别:环境差异少一种,半夜被叫起来排查的理由就少一种。
Go 还自带一套诊断工具。pprof 看 CPU 和内存,runtime/trace 看调度和 GC,GODEBUG 调运行时行为。服务变慢的时候你能定位到具体函数、具体哪一行在分配,而不是拍脑袋加机器。(来源:Go Diagnostics)
很多人说 Java 项目"重"。这个感受是真实的,但锅不该全让语言背。
重的是层层叠上去的东西。一个很简单的接口,走完 controller、service、manager、repository、几张 DTO 和 VO,再加几个自动装配和 yaml,等你看完整条链路会发现业务逻辑只有十行。这个成本不是 JVM 带来的,是工程习惯攒出来的。
Go 更倾向于直给:结构体装数据,接口描述需要的行为,组合把零件拼起来。对一个职责清楚的微服务来说,你很容易看清请求从哪进、经过谁、错了怎么返回。
公平地讲,Java 也没停。Spring Boot 支持 GraalVM Native Image,能明显压低启动时间和内存占用,代价是构建更麻烦、反射之类的动态特性受限。所以比较之前得先明确:你比的是传统 JVM 部署,还是原生镜像方案,这两个的结论完全不一样。(来源:Spring Boot 官方文档)
我的倾向是:新建的轻量网络服务,Go 一般能用更少的工程负担满足需求;已经有成熟 Java 团队和一堆业务资产的,迁移成本得一起算进去。
最后说点实在的。语言只是起点,省资源这件事得落到代码里。
一个没有上限的缓存、一个不会退出的后台任务、一个每次请求都分配大对象的接口,都能把 Go 服务的内存吃上去。Go 给了 GOGC 和 GOMEMLIMIT 这类旋钮,但它们是取舍,不是免费午餐:GOGC 默认 100,调低换内存,多出来的 CPU 花在回收上;GOMEMLIMIT 是软限制,官方文档明确说了不能当成"进程绝对不会超过"的保证。(来源:Go 垃圾回收指南)
所以上线前该做的事还是那几件:按真实负载压一遍,看内存峰值、吞吐和尾延迟,再决定实例规格。别拿开发机上那点流量当依据。
还有个建议是给团队的:可以先选 Go,但别急着把系统拆成十几个微服务。先把模块边界划清楚,等真的出现"这块要独立扩容"或者"这块归另一拨人"的时候再拆。为了微服务而微服务,只是把复杂度从代码里搬到网络上。
说到底,我推荐 Go 不是因为语法优雅——它一点也不优雅,error 处理能把你写烦。是因为它把快速构建、轻量并发、简单部署和不错的资源效率,凑成了一套不用额外操心的工程体验。
对于要频繁发版、独立运行、按需扩容的服务,这些好处不是写在 PPT 上的优点,是你每天都会碰到的。
老张点评:关于 Go 和微服务,网上讨论最多的往往是"谁更快",但真正影响日常的是另外两件事——你多久能拿到可验证的结果,出问题时能不能定位。语言选型说到底是在给这两件事定价。本文的结论对新建的轻量服务成立;要是你手上是跑了八年的 Java 单体,我建议先把迁移成本算清楚再谈换语言。
相关阅读:这篇聊的是「Go 适不适合」,配套还有一篇《为什么新项目放弃 Java/Spring 改用 Golang?一份来自一线的成本对比》,那篇讲成本账,这篇讲工程手感。
本文由老张整理编写,信息综合自 Go 官方文档、Effective Go、Go 垃圾回收指南、OpenJDK JEP 444、Spring Boot 官方文档等公开资料。