前言
在Go语言诞生之前,谷歌主要使用C++和Java进行系统编程和后端服务开发。这些语言虽然功能强大,但也有显著的缺陷:
- 编译速度慢:C++的大型代码库需要很长的编译时间,这在快速开发和迭代中是一个严重的瓶颈。
- 复杂的依赖管理:大型项目中,C++和Java的依赖管理和编译链接过程非常复杂,导致开发和维护困难。
- 并发处理的复杂性:随着互联网服务的规模增长,并发处理成为关键问题。然而,C++和Java在处理并发时需要大量复杂的代码,容易出现错误。
简单的GO项目
一个完整最小的GO项目结构如下
go-test/ 项目根目录 ├── cmd/ │ └── go-test/ │ └── main.go 程序启动入口,负责调用内部 CLI 逻辑 ├── internal/ 项目内部实现目录,禁止外部模块直接引用 │ └── cli/ │ ├── cli.go CLI 主流程、参数分发、hello/greet 等通用命令 │ └── ws.go WebSocket 相关命令实现,如 ws server / ws client ├── go.mod Go 模块定义文件,声明模块名、Go 版本、直接依赖 ├── go.sum Go 依赖校验文件,记录依赖哈希,保证下载内容一致 └── README.md 项目介绍、目录说明、运行方式和扩展说明项目主要功能,在go-test目录下:
go run ./cmd/go-test hello go run ./cmd/go-test greetsum123.5go run ./cmd/go-test ws server go run ./cmd/go-test ws client--message"hello from go client"编译速度
Go 编译快,主要靠 3 件事:语言本身简单、依赖模型简单、工具链统一。
可以理解:Go 从语言设计开始,就在避免那些会让编译器“反复做重活”的机制。
没有
#include那种文本展开,而是import "fmt",这不是把 fmt 的源码文本拷进来,而是:- 依赖按 包 管理, Go 的基本单位是 package
- 包先编译好,会产出这个包的编译结果和导出信息
- 别的包依赖它时,不需要重新完整分析它的源码
- 同包文件一起编译
- 包和包之间通过 import 连接
- 下游包主要看上游包的导出接口
Go 的语言特性比较克制, Go 语法和类型系统是故意做得比较简的。
- 宏系统
- 过度花哨的编译期计算 (Java 中的 Lombok)
- 非常深的继承体系
- 大量隐式规则 (JAVA 中的 annotation processor)
依赖图是强约束的,而且禁止循环依赖,Go 的包依赖必须是一个 DAG,也就是有向无环图。 也就是说:
- A 可以依赖 B
- B 可以依赖 C
- 但 C 不能再回头依赖 A
所以 Go的“编译速度快的原因” 不是要让编译器特别聪明,而是尽量别给编译器制造太多麻烦。另外“官方工具链统一”,也少了很多额外损耗 .
go build 自带构建缓存
- 没变的包,不重编
- 只重编改过的包,以及依赖它的包
- 很多包还能并行编译
依赖管理
一个普通 Go 项目,依赖管理核心通常就这两个文件
- go.mod 依赖声明
## 模块名module go-test## Go 版本go1.22## 直接依赖require github.com/gorilla/websocket v1.5.3 - go.sum 依赖较验
## github.com/gorilla/websocket 这个模块的 v1.5.3 版本,源码包整体内容的校验值是这个## GO要知道下载到的源码内容有没有变github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=## github.com/gorilla/websocket 这个模块的 v1.5.3 版本中,它自己的 go.mod 文件,校验值是这## GO要知道 依赖自己的模块声明文件有没有变github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
GO的依赖管理的核心理念是“我依赖谁,版本是什么,校验值是什么”,并不承受包管理之外的 “建生命周期、插件平台、企业治理 ”, 以MAVEN为例
| 维度 | Go | Maven |
|---|---|---|
| 核心文件 | go.mod+go.sum | 主要是pom.xml,但常连着 parent POM、profiles、plugins、repositories 等 |
| 项目身份定义 | module xxx | groupId + artifactId + version |
| Go/Java 版本声明 | go 1.22直接写在go.mod | 常放在properties或maven-compiler-plugin |
| 直接依赖声明 | require 模块 版本 | <dependency>+groupId/artifactId/version |
| 依赖声明语法复杂度 | 很轻,接近一行一句话 | XML 层级更重 |
| 依赖校验 | go.sum明确记录哈希 | 主要依赖 Maven 仓库机制和 checksum |
| 测试依赖管理 | 也走go.mod/go.sum,但靠*_test.go+go test区分 | 通过scope=test区分 |
| 是否有显式 scope | 没有 Maven 那套compile/test/runtime/provided | 有完整 scope 体系 |
| 测试依赖的区分方式 | 靠测试文件命名和测试命令路径 | 靠依赖声明里的scope=test |
| 版本治理层次 | 相对扁平,当前模块直接require | 常有 parent POM、dependencyManagement、继承覆盖 |
| 本地联调依赖 | replace old/module => ../local-module | 常靠本地安装、多模块、私服、改版本 |
| 排除问题依赖 | exclude module version | <exclusions>更偏排除某条传递依赖链上的包 |
| 工具链入口 | go build/go test/go mod tidy/go get | mvn package/mvn test/ 插件 goal 等 |
并发处理
GO中的并发设计,趋向于把“并发流程表达清楚”, 即通过以下组件
| Go 概念 | Go 常见写法 | Java 中最接近的对应物 | 主要作用 |
|---|---|---|---|
| goroutine | go f() | ExecutorService.submit(...)/Thread.startVirtualThread(...) | 启动一个并发任务 |
| channel | ch <- v/v := <-ch | BlockingQueue/SynchronousQueue/Future/CompletableFuture | 在并发任务之间传数据、同步协作 |
| select | select { ... } | CompletableFuture.anyOf(...)/BlockingQueue.poll(...)/Selector | 同时等待多个事件,谁先就绪就处理谁 |
| context | context.WithCancel/WithTimeout/WithDeadline | Future.cancel(...)/interrupt/ 超时 API / 自定义上下文对象 | 取消、超时、截止时间、请求链路传递 |
| WaitGroup | wg.Add()/wg.Done()/wg.Wait() | CountDownLatch/CompletableFuture.allOf(...) | 等一批并发任务执行结束 |
| mutex | sync.Mutex | synchronized/ReentrantLock | 保护共享变量、临界区互斥 |
| atomic | sync/atomic | AtomicInteger/AtomicLong/VarHandle | 做无锁原子操作 |
| worker pool | goroutine + chan + WaitGroup | ThreadPoolExecutor + BlockingQueue + Future | 控制并发度,批量处理任务 |
| timeout | select + time.After(...)/context.WithTimeout(...) | Future.get(timeout)/orTimeout(...)/ScheduledExecutorService | 给任务或等待过程加超时 |
| graceful shutdown | close(ch)/cancel()/wg.Wait() | shutdown()/shutdownNow()/awaitTermination()/ 中断机制 | 优雅退出后台任务和服务 |
再由 runtime 再帮GO调度。即Go 通过 runtime 统一并发标准的优势主要是:
- 并发模型更统一:启动、通信、等待、取消、退出是一套体系
- 阻塞代码也能高并发:runtime 帮你做调度和 netpoll
- 更适合表达任务协作:channel + select 很自然
- 取消链路标准化:context 贯穿请求到下游
- 更适合服务端工程:团队更容易形成一致写法
go runtime 简析
golang 的 runtime 在 golang 中的地位类似于 Java 的虚拟机,不过 go runtime 不是虚拟机. golang 程序生成可执行文件在指定平台上即可运行,效率很高, 它和 c/c++ 一样编译出来的是二进制可执行文件. 它是像服务代码的一部分,跟着项目一起编译、一起部署、一起运行
运行 golang 的程序并不需要主机安装有类似 Java 虚拟机之类的东西,那是因为在编译时,golang 会将 runtime 部分代码链接进去.
golang 的 runtime 核心功能包括以下内容:
- 协程(goroutine)调度(并发调度模型)
- 垃圾回收(GC)
- 内存分配
内存分配
Go runtime 的内存分配就采用了 Tcmalloc 算法.其核心思想是多级管理,从而降低锁的粒度.
Go 程序在启动时,会首先向系统申请一块内存(虚拟地址空间),然后自己切成小块进行管理. 将申请的内存,分成 3 个区域,spans、bitmap、arena,这三个区域的作用如下.
- arena: 就是堆区,go runtime 在动态分配的内存都在这个区域,并且将内存块分成 8kb 的页,一些组合起来的称为 mspan,成为 go 中内存管理的基本单元,这种连续的页一般是操作系统的内存页几倍大小.
- bitmap: 顾名思义,用来标记堆区使用的映射表,它记录了哪些区域保存了对象,对象是否包含指针,以及 GC 的标记信息.
- spans: 存放 mspan 的指针,根据 spans 区域的信息可以很容易找到 mspan. 它可以在 GC 时更快速的找到的大块的内存 mspan.
垃圾回收(GC)
垃圾回收机制是编程语言的重要部分,它影响到程序的长久稳定运行. Java、Python 等语言都有自己的垃圾回收机制,而不需要像 c/c++一样由程序员管理,可以避免大量的内存泄漏.
GO也采用了与JAVA相似的《三色标记清扫法》来处理GC
协程调度模型
golang 语言相比其它语言有一个特殊之处,它实现了自己的调度模块,并不完全是由计算机操作系统进行调度的(进程、线程). golang 原生支持协程 goroutine,区别于线程、进程. goroutine 的调度由 go runtime 进行,这也是 golang 并发效率高的原因之一.
go 在处理协程上,使用了 GPM 调度模型,从而支持高效的并发调度. 内核线程与逻辑处理器是多对多的关系即 M:N. 从而提升并发效率. GPM 各个模块的解释如下:
- G: 即 Goroutine,更轻量级的线程,保存着上下文信息.
- P: Processor,是逻辑处理器. 将 goroutine 绑定逻辑处理器 P 的本地队列后,才会被调度. Processor 提供了相关的执行环境(Context),如内存分配状态(mcache),任务队列(G)等
- M: 它才是真正的计算资源,是系统线程.
- 全局队列(Global Run Queue): 未分配 Processor 的 Goroutine 保存在全局队列中. Processor 或 M 都可以从全局队列中取出 G .
- 本地队列(Local Run Queue): 是 Processor 的队列,当队列为空时,会从全局队列或其它队列补充 Goroutine.
- sysmon 协程: go runtime 会创建一个 sysmon 协程. 它会定期唤醒检查 goroutine 和 processor,确保 goroutine 不会长期占用 CPU 以及 Processor 可以被执行.
总结
Go的核心思想,就是用统一的默认约定、内置的 runtime 能力和简化的工程路径,替开发者收掉大量“非业务复杂度”,让程序员更专注于业务流程和系统协作本身。
Go 不是想给你最多选择,而是想给你一条默认就比较对的路。
站在巨人的肩膀上
《go runtime 简析》
《万字长文深入浅出 Golang Runtime》