news 2026/9/7 13:31:25

【DIY系列:Java虚拟机】第04篇:Go 语言环境准备——用 Go 写 JVM 的 5 大理由

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【DIY系列:Java虚拟机】第04篇:Go 语言环境准备——用 Go 写 JVM 的 5 大理由

上一篇【第03篇】JDK 安装与环境准备——工欲善其事必先利其器
下一篇【第05篇】项目结构与第一行代码——jvmgo 项目骨架搭建


摘要

HotSpot 用 C++ 写了 150 万行,那我们用什么写?答案是 Go。

本文先给出用 Go 写 JVM 的五大理由(语法简洁、自带 GC、标准库齐全、跨平台、学习曲线低),并横向对比 C++/Java/Rust 的优劣。然后完成 Go 环境的安装、GOPATH 与 Go Modules 的配置、跑通第一个 Go 程序。最后,用最短的篇幅速成本系列需要的 Go 核心语法——结构体、接口、切片、指针、多返回值。看完这篇,你就能开始写 jvmgo 了。


一、为什么用 Go 写 JVM?

先说结论:Go 是写 JVM 这种"充满引用的复杂数据结构"程序的绝佳选择

我们横评一下四种候选语言:

维度C++JavaRustGo
语法复杂度极高(模板/宏/头文件/内存管理)高(所有权/借用/生命周期)极低
内存管理手动(new/delete,容易泄漏)GC编译期所有权检查GC
编译速度慢(大型项目分钟级)很慢极快(秒级)
标准库一般(STL 有限)强(zip/binary/文件开箱即用)
写 VM 的痛苦度⭐⭐⭐⭐⭐⭐⭐⭐(鸡生蛋悖论)⭐⭐⭐⭐(借用检查器打架)
性能⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
学习曲线陡峭平缓陡峭平缓

具体到写 JVM 这件事上,Go 的五大优势是:

优势 1:语法极简,没有"语言噪音"

C++ 写 JVM,你得跟模板、宏、头文件、多重继承搏斗;Go 只有 25 个关键字,没有继承(用组合替代),没有泛型(Go 1.18 前),没有异常(用 error 返回值)。

写一个指令接口,Go 是这么写的:

// Go:清爽typeInstructioninterface{FetchOperands(reader*BytecodeReader)Execute(frame*rtda.Frame)}

对比 C++ 的写法,你得写头文件、声明虚基类、处理析构……代码量翻几倍。

优势 2:自带 GC,不用管内存

JVM 里到处都是对象引用、类引用、栈帧引用。用 C++ 你得小心翼翼管理内存;用 Go,直接交给 GC

// 不用 new/delete,不用智能指针,不用 RAIIframe:=&rtda.Frame{localVars:newLocalVars(maxLocals),operandStack:newOperandStack(maxStack),}// 用完了?GC 会回收,你不用管

重点:这一点太重要了。我们用 Go 写 JVM,内存管理交给 Go 的 GC,我们自己不用实现 GC——这直接砍掉了 JVM 最复杂的模块之一。

优势 3:标准库齐全,二进制处理开箱即用

写 JVM 有大量重复工作:读 zip(JAR 包)、解析二进制(class 文件)、处理字节序(Big-Endian)。Go 标准库全都有:

import("archive/zip"// 读 JAR 包"encoding/binary"// 大端序解析二进制"os"// 文件操作"path/filepath"// 跨平台路径处理)// 读大端序 uint32(class 文件的魔数),一行搞定magic:=binary.BigEndian.Uint32(data[0:4])

这些在 C++ 里要么自己写,要么引入第三方库。

优势 4:跨平台,编译快

Go 原生支持交叉编译,一份代码编译出 Windows/Linux/macOS 三个平台的可执行文件。而且编译速度极快——jvmgo 这种万行级项目,编译只要几秒。

GOOS=windowsGOARCH=amd64 go build# 编 Windows 版GOOS=linuxGOARCH=amd64 go build# 编 Linux 版GOOS=darwinGOARCH=arm64 go build# 编 Mac (M系列) 版

优势 5:学习曲线低,有 C/Java 基础几天上手

Go 的语法对 C/Java 程序员极其友好。如果你写过 Java,看 Go 代码基本没有障碍——只是把class换成struct,把interface的实现从implements改成隐式实现(duck typing)。

【语言选择决策树】 想写一个 JVM,选什么语言? │ ┌───────┴───────┐ │ │ 追求极致性能? 追求快速实现? │ │ ▼ ▼ C++ / Rust Go ← 我们在这儿 (HotSpot 同款) (快速验证原理) │ │ ▼ ▼ 150万行代码 1万行代码 学 3 个月 学 3 天 调试痛苦 调试轻松

重点:我们的目标是理解 JVM 原理,不是造一个生产级 JVM。用 Go 能把精力 100% 放在原理本身,而不是跟语言细节搏斗。


二、Go 的下载与安装

Windows

  1. 访问 Go 官网下载页(golang.org/dl)或国内镜像(golang.google.cn/dl)
  2. 下载go1.x.x.windows-amd64.msi
  3. 双击安装,默认路径C:\Go
  4. 安装程序会自动把C:\Go\bin加到 PATH

macOS

# 方式一:Homebrewbrewinstallgo# 方式二:下载 pkg 安装包

Linux

# 下载并解压wgethttps://golang.google.cn/dl/go1.21.0.linux-amd64.tar.gzsudotar-C/usr/local-xzfgo1.21.0.linux-amd64.tar.gz# 配置 PATH(加到 ~/.bashrc 或 ~/.zshrc)exportPATH=$PATH:/usr/local/go/bin

验证安装

go version# 输出:go version go1.21.0 windows/amd64

如果提示go: command not found,检查 PATH 配置。


三、GOPATH 与 Go Modules

这是 Go 新手最容易懵的地方。先理清两个概念:

GOPATH(传统方式,Go 1.11 之前)

GOPATH 是 Go 的工作目录,所有 Go 项目都必须放在$GOPATH/src下:

【GOPATH 目录结构】 $GOPATH/ 例如 D:\go\workspace ├── src/ ← 源码目录(你的项目放这里) │ └── jvmgo/ │ ├── ch01/ │ ├── ch02/ │ └── ... ├── pkg/ ← 编译后的包文件(.a) └── bin/ ← 编译后的可执行文件

《自己动手写 Java 虚拟机》原书用的是 GOPATH 模式,项目路径类似D:\go\workspace\src\jvmgo\ch01

Go Modules(现代方式,Go 1.11+ 推荐)

Go Modules 是官方推荐的依赖管理方式,项目可以放在任意目录,通过go.mod文件管理依赖:

# 初始化模块go mod init jvmgo# 生成的 go.mod 文件module jvmgo go1.21

重点:本系列采用Go Modules方式(更现代、更灵活),但目录组织上依然参考原书的ch01 ~ ch11渐进式结构。两种方式都能跑,选你顺手的即可。

配置 GOPATH(可选)

# 查看当前 Go 环境配置goenv# 设置 GOPATH(如果用 Modules,不配也行)goenv-wGOPATH=D:\go\workspace# 设置国内代理(加速依赖下载,强烈推荐)goenv-wGOPROXY=https://goproxy.cn,direct

提示:国内网络访问 Go 官方源比较慢,一定要配GOPROXY=https://goproxy.cn,否则下载依赖会卡到怀疑人生。


四、第一个 Go 程序

按惯例,先跑个 Hello World 验证环境。

// main.gopackagemainimport"fmt"funcmain(){fmt.Println("Hello, jvmgo!")}

运行:

# 方式一:直接运行(不生成可执行文件)go run main.go# 方式二:编译后运行go build-ojvmgo.exe main.go ./jvmgo.exe

输出Hello, jvmgo!,说明环境 OK。

常用 Go 命令先记一下:

命令作用
go run xxx.go直接运行源码(开发调试用)
go build编译生成可执行文件
go build -o name指定输出文件名
go test运行单元测试
go fmt格式化代码(Go 强制统一格式)
go mod init初始化模块
go get下载依赖
go env查看环境配置

五、Go 语法速成:写 JVM 需要的那些

如果你写过 Java 或 C,下面这些语法基本扫一眼就会。本系列用到的 Go 特性不多,掌握这些就够了。

1. 变量与类型

// 显式声明varversionuint32=52varnamestring="java/lang/Object"// 短声明(最常用,:= 自动推导类型)magic:=0xCAFEBABE// intclassName:="HelloWorld"// stringok:=true// bool// 批量声明var(maxStackuint=100maxLocalsuint=100)

常用类型对照:

Go 类型说明Java 对照
int8/16/32/64有符号整数byte/short/int/long
uint8/16/32/64无符号整数
byteuint8别名
float32/64浮点float/double
string字符串String
bool布尔boolean
[]T切片(动态数组)数组/ArrayList
map[K]V映射HashMap
*T指针引用

2. 结构体(对标 Java 的类)

// 定义结构体typeClassstruct{accessFlagsuint16namestringsuperClassNamestringinterfaceNames[]stringconstantPool*ConstantPool}// 创建实例c:=&Class{// & 取地址,得到指针accessFlags:0x0021,// ACC_PUBLIC | ACC_SUPERname:"HelloWorld",}// 访问字段fmt.Println(c.name)

3. 方法(带接收者)

Go 没有 class,方法定义在结构体外面,通过"接收者"绑定:

// 指针接收者(可以修改结构体内容)func(c*Class)Name()string{returnc.name}// 值接收者(只读)func(c Class)String()string{return"Class: "+c.name}

4. 接口(隐式实现,duck typing)

这是 Go 最优雅的设计之一——不需要implements关键字,实现了接口的所有方法,就自动实现了这个接口

// 定义接口typeConstantInfointerface{readInfo(reader*ClassReader)}// 实现接口:只要实现了 readInfo 方法,就自动是 ConstantInfotypeConstantIntegerInfostruct{valint32}func(self*ConstantIntegerInfo)readInfo(reader*ClassReader){self.val=int32(reader.readUint32())}// 使用:可以直接放进接口切片varpool[]ConstantInfo pool=append(pool,&ConstantIntegerInfo{})

重点:这个特性在解析 class 文件常量池时会被用到极致——14 种常量各写一个结构体,都实现ConstantInfo接口,然后统一放进[]ConstantInfo数组里管理。

5. 切片(动态数组)

// 创建slots:=make([]Slot,10)// 长度 10 的切片varframes[]*Frame// nil 切片// 追加frames=append(frames,newFrame)// 切片操作top:=slots[0:2]// 取前 2 个(左闭右开)// 长度与容量len(slots)// 长度cap(slots)// 容量

6. 指针

x:=10p:=&x// p 是指向 x 的指针*p=20// 通过指针修改值(解引用)fmt.Println(x)// 20// 结构体指针可以直接用 . 访问字段(Go 自动解引用)frame:=&Frame{}frame.nextPC=10// 等价于 (*frame).nextPC = 10

7. 多返回值与错误处理

// Go 惯用法:返回值 + errorfuncreadClass(namestring)([]byte,error){data,err:=os.ReadFile(name)iferr!=nil{returnnil,err// 出错返回 error}returndata,nil// 成功返回数据 + nil}// 调用data,err:=readClass("HelloWorld.class")iferr!=nil{panic(err)// 或者 log.Fatal(err)}

重点:Go 没有 try-catch,错误处理靠显式返回 error 并检查。刚开始觉得啰嗦,习惯了会觉得比异常清晰。

8. 位运算(写 JVM 大量使用)

// 访问标志、类索引这些大量使用位运算const(ACC_PUBLIC=0x0001ACC_FINAL=0x0010ACC_SUPER=0x0020)// 判断某位是否设置ifflags&ACC_PUBLIC!=0{fmt.Println("是 public 类")}// 组合标志flags:=ACC_PUBLIC|ACC_SUPER// 0x0021// 移位majorVersion:=uint16(data[6])<<8|uint16(data[7])

9. 类型转换

Go 的类型转换必须显式,不会自动转换:

varbbyte=0x12variint32=int32(b)// 必须显式转换varuuint32=52vari32int32=int32(u)// uint32 → int32 也要显式// 浮点数的特殊处理(解析 double 常量时会用到)bits:=binary.BigEndian.Uint64(data)val:=math.Float64frombits(bits)// 位模式 → float64

10. 包与导入

packageclasspath// 声明包名import("fmt"// 标准库"jvmgo/ch06/classfile"// 本项目内的包(模块名 + 路径))// 大写字母开头的标识符才对外可见(导出)funcNewClasspath(...)*Classpath{...}// 可导出funcparseEntry(...){...}// 小写,包内私有

重点:Go 用首字母大小写控制可见性——大写 = public,小写 = private。这点和 Java 的public/private关键字不同,刚开始容易踩坑。


六、IDE 与调试

推荐两款:

IDE优点适合
VS Code + Go 插件轻量、免费、插件生态好大多数人
GoLand(JetBrains)功能最强、重构/调试体验好专业 Go 开发者 / 追求效率

VS Code 配置很简单:

  1. 安装 VS Code
  2. 安装 Go 官方插件(golang.go)
  3. 打开 Go 文件,插件会提示安装工具链(gopls、dlv 等),点"Install All"

调试配置(.vscode/launch.json):

{"version":"0.2.0","configurations":[{"name":"Launch jvmgo","type":"go","request":"launch","mode":"auto","program":"${workspaceFolder}/ch01","args":["-cp","./test","HelloWorld"]}]}

配置好后按 F5 就能断点调试我们的 jvmgo——后面调试字节码执行过程时,这个能力会非常有用。


本篇小结

我们选 Go 作为 jvmgo 的实现语言,五大理由:

  1. 语法极简:没有模板/宏/头文件地狱,25 个关键字搞定一切
  2. 自带 GC:不用自己实现垃圾回收,专注 JVM 逻辑本身
  3. 标准库齐全:archive/zip、encoding/binary 开箱即用
  4. 跨平台 + 编译快:秒级编译,一键交叉编译三平台
  5. 学习曲线低:有 C/Java 基础几天上手

环境方面完成了:Go 安装与验证、GOPATH/Go Modules 的概念梳理、国内代理 GOPROXY 配置、第一个 Go 程序跑通。语法方面速成了结构体、接口、切片、指针、多返回值、位运算等写 JVM 的核心语法。

下一篇,终于要动手写代码了!我们会创建 jvmgo 项目骨架,实现java命令的雏形——解析命令行参数、打印版本号、输出 usage 帮助。这是 ch01 的全部内容,也是我们 11 章旅程的第一步。


上一篇【第03篇】JDK 安装与环境准备——工欲善其事必先利其器
下一篇【第05篇】项目结构与第一行代码——jvmgo 项目骨架搭建


版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/7 13:29:50

西藏新版预算定额宣贯材料解读:高原系数与结算应用要点

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 13:25:48

12G显存本地跑4K视频生成:ComfyUI部署与优化实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 13:25:45

CATIA V5 AI智能体:从自然语言到自动化建模的工程实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 13:25:43

基于Arduino与振动传感器的智能闹钟开发全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 13:24:48

STM32G071 USART3不定长收发:DMA+空闲中断与环形缓冲设计

简介&#xff1a;STM32G071CBT6微型开发板串口3不定长可变长数据报文收发程序&#xff0c;是一份面向嵌入式开发者、围绕STM32G071CBT6的USART3通信实践而整理的源码工程。资源以Keil MDK工程为主体&#xff0c;包含663个C源文件、261个头文件&#xff0c;以及IAR/STM32CubeMX相…

作者头像 李华