常见问题 (FAQ)
起源
初衷是什么?
2007年Go语言诞生时,编程世界与今天大不相同。当时生产环境中的软件通常使用C++或Java编写,GitHub还不存在,大多数计算机还都不是多处理器,除了Visual Studio和Eclipse之外,几乎没有什么集成开发环境(IDE)或其他高级工具,更不用说在互联网上免费提供了。
与此同时,我们对使用当时语言及其相关构建系统来构建大型软件项目所需的过度复杂性感到沮丧。自C、C++和Java等语言最初开发以来,计算机的速度已大幅提升,但编程本身并没有取得太大进展。此外,多处理器正变得无处不在,但大多数语言在如何高效安全地编程方面提供的帮助很少。
我们决定退一步,思考随着技术发展,未来几年哪些重大问题将主导软件工程,以及一种新语言可能如何解决这些问题。例如,多核CPU的兴起表明,语言应提供对某种并发或并行计算的一等支持。为了使资源管理在大型并发程序中易于处理,垃圾回收(或至少是某种安全的自动内存管理)是必需的。
这些考量促成了一系列讨论,Go由此诞生——最初是一系列想法和需求,随后发展成了一门语言。 一个首要目标是:Go应该通过支持工具化、自动化格式化代码等日常任务、并消除大规模代码库带来的障碍,来帮助工作中的程序员做更多事情。
关于Go的目标及其实现方式(至少是接近实现的方式),在文章《Go之于Google:服务于软件工程的语言设计》中有更为详尽的描述。
项目历史是怎样的?
项目历史
2007年9月21日,Robert Griesemer、Rob Pike和Ken Thompson在白板上勾勒出了新语言的设计目标。
几天内,团队就明确了具体计划及方向。随后在兼职推进其他工作的同时,持续进行语言设计。
至2008年初,Ken开始着手开发编译器原型(输出C代码)用于验证概念。
同年年中,该项目升级为全职项目,并具备了开发生产级编译器的条件。
2008年5月,Ian Taylor基于草案规范,独立为Go语言开发了GCC前端。
Russ Cox于2008年末加入,推动语言特性和标准库从原型走向成熟。
2009年11月10日,Go正式成为开源项目。
社区成员贡献了大量代码、讨论与创意。
如今全球已有数百万Go程序员(Gopher),数量持续增长。
Go的成功已远超团队最初预期。
地鼠吉祥物的起源
吉祥物和标志由Renée French设计,她也是Plan 9系统兔子Glenda的设计者。
官方博客文章解释了该地鼠形象源自她多年前为WFMU电台T恤设计的图案。
该标志和吉祥物采用知识共享署名4.0许可协议授权。
地鼠有专门的角色设定图,详细说明了其特征及标准绘制方法。该设定图首次公开于2016年Gophercon大会上Renée的演讲。
它具有独特特征——是专属的"Go地鼠",而非普通地鼠形象。
语言名称是Go还是Golang?
官方名称是"Go"。
"Golang"的称呼源于早期官网域名golang.org(当时尚无*.dev*域名)。
尽管许多人使用golang作为标签(如社交媒体话题"#golang"),但语言官方名称始终是简短的"Go"。
补充说明:虽然官方标志包含两个大写字母,但书写语言名称时应为"Go"而非"GO"。
为何要创造新语言?
Go诞生于我们对Google现有语言和环境的不满。当时的编程变得异常艰难,语言选择难辞其咎:开发者必须在高效编译、高效执行和编程便捷性三者中取舍——主流语言无法同时满足这三项。许多程序员为追求效率转向动态类型语言(如Python/JavaScript),而非C++(或相对易用的Java),实则牺牲了类型安全与执行效率。
这一困境并非个例。在多年沉寂后,Go与Rust、Elixir、Swift等语言共同掀起新一轮语言革新热潮。
Go的核心突破在于:
- 融合动态类型语言的编程便捷性与静态编译型语言的效率与安全性
- 针对现代硬件优化:原生支持网络服务和多核计算
- 极致开发体验:单机构建大型程序仅需数秒
为实现这些目标,我们重构了编程范式:
| 传统方案 | Go的创新 |
|---|---|
| 类型继承体系 | 组合式类型系统 |
| 手动内存管理 | 并发与垃圾回收 |
| 隐式依赖 | 显式依赖声明 |
这些特性无法通过库或工具实现,唯有新语言能承载。
详见《Go之于Google》,该文深入探讨了设计动机,并对此FAQ中诸多问题给出更详实的背景说明。
(注:采用技术白皮书式结构化表达,通过对比表格突出创新点;关键术语使用加粗强调;保留原文超链接;"fast"译为"极致开发体验"以完整传递"秒级构建"的潜台词)
语言渊源
Go主要源自C语言家族(基础语法),同时吸收了:
- Pascal/Modula/Oberon家族的声明语法和包机制
- CSP理论衍生语言(如Newsqueak/Limbo)的并发思想
但Go是全方位创新的语言——每个细节都基于对"程序员实际需求"的深度思考,旨在提升编程效率(以及趣味性)。
设计哲学
核心矛盾
设计Go时,Java/C++是服务端主流语言(至少在Google如此)。但团队认为:
- 这些语言需要过多模板代码
- 程序员为效率转向Python等动态语言,却牺牲了类型安全和执行效率
Go的突破点在于:单语言同时实现高效、安全与灵活
减法原则
- 零冗余设计:无前置声明/头文件,所有元素仅声明一次
- 智能初始化:自动类型推导(
:=声明并初始化) - 极简语法:轻量关键字,消除重复代码(如
new(foo.Foo)可简化为myFoo := &Foo{}) - 无类型继承:类型即接口,无需显式声明关系
- 正交性:
✓ 方法可绑定任意类型
✓ 结构体承载数据,接口定义抽象
✓ 组合优于继承
这些设计使Go在保持高表达力的同时,不增加认知负担,实现"简单即高效"。
(注:采用分层标题突出逻辑;关键创新点用粗体标记;用代码示例对比说明语法优势;"orthogonal"译为"正交性"保留数学概念;通过✓符号列表提升可读性)
应用实践
Google内部是否使用Go?
是的。Go在Google内部生产环境广泛使用。典型例子如下载服务器dl.google.com,负责分发Chrome二进制包、apt-get等大型安装包。
虽然Go并非Google唯一语言(远非如此),但它是多个核心领域的关键语言:
- 站点可靠性工程(SRE)
- 大规模数据处理
- 谷歌云计算平台核心组件
哪些公司在用Go?
Go在全球快速增长,尤其在(但不限于)云计算领域:
能否与C/C++程序交互?
技术可行但有代价:
- 需专用接口层,且会破坏Go的内存安全与栈管理特性
- 仅建议必要时调用C库,且需谨慎处理风险
多编译器支持:
| 编译器 | 特性 |
|---|---|
gc | Go官方标准编译器,使用独立调用约定,需通过cgo交互 |
gccgo/gollvm | 兼容传统ABI,可直接链接GCC/LLVM编译的C/C++代码(需深入理解调用约定) |
TinyGo | 特殊用途子集编译器 |
SWIG工具可进一步支持C++库集成。
IDE支持如何?
Go虽无官方定制IDE,但代码分析友好的设计使得主流编辑器/IDE均提供良好支持:
- 官方方案:
gopls语言服务器(LSP协议) - 主流支持:VSCode、IntelliJ(GoLand)、Eclipse、Vim、Emacs等均有插件或原生支持
支持Protocol Buffers吗?
通过独立开源项目实现:github.com/golang/protobuf
提供编译插件及配套库。
设计哲学
Go需要运行时吗?
是的,但与传统理解不同。Go的运行时库(通常简称runtime)提供以下核心功能:
- 垃圾回收
- 并发调度
- 栈管理
与C的libc类似,但不包含虚拟机。Go程序会直接编译为本地机器码(或JavaScript/WebAssembly),因此"runtime"在Go中仅指提供语言服务的库,而非托管执行环境。
关于Unicode标识符
Go设计时有意突破ASCII限制,允许使用Unicode字母/数字作为标识符。但存在两个重要限制:
- 排除组合字符:如梵文等组合文字无法使用
- 导出规则冲突:因导出标识符需首字母大写,导致部分语言字符(如中文)永远无法导出
当前解决方案(如X日本語)并不理想。未来可能参考Unicode TR31建议改进,但需保持向后兼容性和大小写可见性规则。
为什么没有特性X?
Go设计聚焦于:
- 编程愉悦性
- 编译速度
- 概念正交性
- 并发/垃圾回收支持
若你钟爱的特性缺失,可能是因为它:
- 不符合设计哲学
- 影响编译效率
- 破坏系统模型简洁性
建议深入探索Go现有特性,或许能找到更优雅的替代方案。
泛型何时引入?
Go 1.18正式加入类型参数支持。详情见:
为何初期没有泛型?
- 原始目标:构建易维护的服务端程序
- 早期重点:可维护性、可读性、并发
- 泛型会增加类型系统和运行时复杂度,团队花费数年寻找平衡方案
为什么不用异常?
Go认为try-catch-finally会导致:
- 代码结构复杂化
- 错误过度分类(如文件打开失败)
Go的方案:
- 多返回值错误处理:天然支持错误码
- 内置
panic/recover:仅处理致命错误(见Defer, Panic, and Recover) - 错误即值:充分利用Go特性构建错误处理链(见Errors are values)
为什么没有断言?
断言虽方便,但易导致程序员逃避错误处理。Go要求显式处理错误,确保:
- 服务持续运行(非致命错误不崩溃)
- 错误信息直接明确(减少堆栈分析负担)
为何基于CSP模型?
传统并发(如pthreads)因过度关注底层细节(锁、条件变量等)而复杂。CSP模型优势:
- 提供高级抽象
- 保持底层效率
- 与过程式语言天然契合(如Occam/Erlang)
Go的并发原语(尤其是一等通道对象)源自CSP家族分支。
为何用Goroutines而非线程?
Goroutines让并发更易用:
- 轻量级:初始栈仅几KB,可动态伸缩
- 自动调度:运行时自动将阻塞的协程迁移到可用线程
- 低成本:每函数调用约3条指令开销
- 高并发:单进程可支持数十万Goroutines(线程通常仅数千)
为什么map操作不设计成原子性的?
经过长期讨论,团队认为map的典型使用场景不需要多goroutine安全访问。若确实需要(通常map是某个更大同步数据结构的一部分),强制所有map操作加锁会:
- 拖慢大多数无需并发安全的程序
- 仅对少数场景提供安全性
需注意:非受控的map更新会导致程序崩溃。但语言本身不禁止原子更新——在需要时(如托管不可信程序),运行时可实现内部锁机制。
安全访问规则:
- ✅ 并发读安全:所有goroutine仅查询/遍历map(
for range) - ❌ 写时危险:存在赋值/删除操作时需外部同步
辅助工具:
- 运行时检测:部分实现会主动报告并发修改错误
sync.Map:适用于静态缓存等特定场景(非常规map替代品)
会采纳我的语言修改建议吗?
虽然社区讨论活跃(见邮件列表),但绝大多数修改提案未被采纳。原因包括:
- 兼容性承诺:Go1兼容保证禁止破坏现有代码
- 设计哲学:需符合《Go之于Google》中的核心目标
- 变更成本:即使兼容Go1,也可能因引入复杂性被拒
未来大版本可能不兼容Go1,但会:
- 保持变更最小化
- 提供旧代码自动迁移路径
类型系统
Go是面向对象语言吗?
是,也不是。关键特性对比:
| 传统OOP | Go的解决方案 |
|---|---|
| 类型继承体系 | 无继承,通过接口隐式满足 |
| 方法绑定对象 | 方法可绑定任意类型(含基本类型) |
| 显式声明类型关系 | 组合+接口自动推导 |
优势:Go的"对象"比C++/Java更轻量,且支持:
- 多接口实现
- 零方法接口(如
interface{}) - 事后扩展接口(无需修改原类型)
如何实现动态方法分发?
仅通过接口实现:
- 接口方法:动态分发
- 结构体/具体类型方法:静态解析
为什么没有类型继承?
传统OOP中类型关系讨论过于复杂,Go采用隐式接口满足:
- 类型自动满足包含其方法子集的接口
- 无需显式声明关系
- 支持多接口、零方法接口
- 可事后添加接口(如测试时)
典型案例:io.Writer接口驱动了fmt.Fprintf/bufio/image等包的解耦设计。
为什么len是函数而非方法?
设计权衡结果:
- 作为函数实现更简洁
- 避免基础类型(如int)的接口复杂性
- 实践中无不良影响
为什么Go不支持方法和操作符重载?
如果方法调度不需要进行类型匹配,将会简化很多。根据其他语言的经验,我们发现虽然同名但不同签名的多种方法偶尔很有用,但在实践中也可能造成混淆和脆弱性。仅通过名称进行匹配并要求类型一致,是Go类型系统中一个主要的简化决策。
至于操作符重载,它看起来更像是一种便利,而非绝对必要。再次强调,没有它,事情会变得更简单。
为什么Go没有“implements”声明?
Go的类型通过实现接口的方法来实现接口,仅此而已。这个特性允许在不修改现有代码的情况下定义和使用接口。它实现了一种结构类型,促进了关注点分离,提高了代码重用性,并使得随着代码发展出现模式时更容易构建。 接口的语义是Go感觉灵活、轻量级的主要原因之一。
更多详细信息,请参阅关于类型继承的问题。
如何确保我的类型满足接口?
你可以通过尝试使用T或指向T的指针的零值进行赋值,让编译器检查类型T是否实现了接口I:
type T struct{}
var _ I = T{} // 确认T实现了I。
var _ I = (*T)(nil) // 确认*T实现了I。如果T(或相应的*T)未实现I,编译时会发现这个错误。
如果你希望接口的用户明确声明他们实现了该接口,可以在接口的方法集中添加一个具有描述名称的方法。例如:
type Fooer interface {
Foo()
ImplementsFooer()
}然后,类型必须实现ImplementsFooer方法才能成为Fooer,清楚地记录这一事实,并在go doc的输出中宣布。
type Bar struct{}
func (b Bar) ImplementsFooer() {}
func (b Bar) Foo() {}大多数代码不使用这种限制,因为它们限制了接口概念的实用性。但有时,它们对于解决类似接口之间的歧义是必要的。
为什么类型T不满足Equal接口?
考虑这个简单的接口,表示一个可以与另一个值进行比较的对象:
type Equaler interface {
Equal(Equaler) bool
}以及这个类型T:
type T int
func (t T) Equal(u T) bool { return t == u } // 不满足Equaler与某些多态类型系统中的类似情况不同,T并不实现Equaler。
T.Equal的参数类型是T,而不是严格要求的Equaler类型。
在Go中,类型系统不会自动提升Equal的参数;这是程序员的责任,如类型T2所示,它确实实现了Equaler:
type T2 int
func (t T2) Equal(u Equaler) bool { return t == u.(T2) } // 满足Equaler但即使这样,也与其他类型系统不同,因为在Go中,任何满足Equaler的类型都可以作为T2.Equal的参数,并且在运行时我们必须检查参数是否为T2类型。一些语言在编译时就能做出这种保证。
相关的例子反过来:
type Opener interface {
Open() Reader
}
func (t T3) Open() *os.File在Go中,T3不满足Opener,尽管在其他语言中可能会。
虽然Go的类型系统在这种情况下为程序员做的事情较少,但缺乏子类型使得关于接口满足的规则非常简单:函数的名称和签名是否与接口的完全一致? Go的规则也很容易高效地实现。我们认为这些好处弥补了自动类型提升的缺失。
我能将[]T转换为[]interface吗?
不能直接转换。
语言规范禁止这样做,因为这两种类型在内存中的表示方式不同。有必要将元素逐个复制到目标切片。以下示例将int的切片转换为interface{}的切片:
t := []int{1, 2, 3, 4}
s := make([]interface{}, len(t))
for i, v := range t {
s[i] = v
}如果T1和T2有相同的基本类型,我能将[]T1转换为[]T2吗?
以下代码样本的最后一行无法编译。
type T1 int
type T2 int
var t1 T1
var x = T2(t1) // 正确
var st1 []T1
var sx = ([]T2)(st1) // 错误在Go中,类型与方法是密切相关的,每个命名类型都有一个(可能为空的)方法集。 通用规则是,你可以更改要转换的类型名称(从而可能更改其方法集),但不能更改复合类型的元素的名称(和方法集)。Go要求你明确地进行类型转换。
为什么我的nil错误值不等于nil?
在内部,接口被实现为两个元素,类型T和值V。
V是一个具体值,如int、struct或指针,从不是接口本身,并且具有类型T。例如,如果我们把整数值3存储在一个接口中,得到的接口值在示意图上是(T=int, V=3)。值V也被称为接口的动态值,因为给定的接口变量在程序执行过程中可能持有不同的值V(和相应的类型T)。
只有当V和T都未设置时,接口值才是nil,即(T=nil, V未设置)。
特别地,一个nil接口将始终持有nil类型。如果我们把一个*int类型的nil指针存储在接口值中,内部类型将是*int,不管指针的值是多少:(T=*int, V=nil)。因此,这样的接口值将是非nil,即使内部的指针值V是nil。
这种情况可能会令人困惑,并且当一个nil值被存储在接口值中时会发生,例如一个error返回值:
func returnsError() error {
var p *MyError = nil
if bad() {
p = ErrBad
}
return p // 将始终返回非nil错误。
}如果一切顺利,函数返回一个nil p,所以返回值是一个持有(T=*MyError, V=nil)的error接口值。这意味着如果调用者将返回的错误与nil比较,即使没有坏事发生,它看起来也总是像一个错误。要向调用者返回一个正确的nil error,函数必须显式返回nil:
func returnsError() error {
if bad() {
return ErrBad
}
return nil
}对于返回错误的函数来说,最好始终在其签名中使用error类型(如我们上面所做的),而不是具体类型如*MyError,以帮助确保错误被正确创建。例如,os.Open
返回一个error,即使不是nil,它总是具体类型
*os.PathError。
只要使用接口,这里描述的情况就可能发生。只要记住,如果任何具体值已经存储在接口中,接口就不会是nil。
更多信息,请参阅反射定律。
为什么零大小类型表现奇怪?
Go支持零大小类型,例如没有字段的结构体(struct{})或没有元素的数组([0]byte)。
零大小类型中无法存储任何值,但在不需要值的情况下,这些类型有时很有用,例如在map[int]struct{}或具有方法但没有值的类型中。
具有零大小类型的不同变量可能会被放置在内存中的同一位置。 这是安全的,因为那些变量中不能存储任何值。
此外,语言对指向两个不同的零大小变量的指针是否相等不做任何保证。
根据程序的编译和执行方式,这种比较甚至可能在程序的一个点返回true,而在另一个点返回false。
与零大小类型相关的另一个问题是,指向零大小结构体字段的指针不得与指向内存中另一个不同对象的指针重叠。 这可能会在垃圾回收器中引起混淆。 这意味着,如果结构体的最后一个字段是零大小,结构体会被填充,以确保指向最后一个字段的指针不与紧随结构体的内存重叠。 因此,这个程序:
func main() {
type S struct {
f1 byte
f2 struct{}
}
fmt.Println(unsafe.Sizeof(S{}))
}在大多数Go实现中将打印2,而不是1。
为什么没有像C那样的无标记联合体?
无标记联合会违反Go的内存安全保证。
为什么Go没有变体类型?
变体类型,也称为代数类型,提供了一种指定值可能是一组其他类型之一的方法,但仅限于那些类型。一个常见的系统编程示例是,指定一个错误是网络错误、安全错误或应用程序错误,并允许调用者通过检查错误的类型来区分问题的来源。另一个例子是语法树,其中每个节点可以是不同的类型:声明、语句、赋值等。
我们考虑过为Go添加变体类型,但经过讨论后决定不添加,因为它们在接口方面有混淆的重叠。如果变体类型的元素本身是接口,会发生什么?
此外,语言已经涵盖了一些变体类型所解决的问题。错误示例很容易通过使用接口值来保存错误,并使用类型开关来区分情况。语法树示例也可以实现,尽管不那么优雅。
为什么Go没有协变结果类型?
协变结果类型意味着像
type Copyable interface {
Copy() interface{}
}这样的接口会被
func (v Value) Copy() Value这个方法满足,因为Value实现了空接口。
在Go中,方法类型必须完全匹配,所以Value不实现Copyable。
Go将类型的功能(其方法)与类型的实现分开。如果两个方法返回不同的类型,它们就不是在做相同的事情。希望有协变结果类型的程序员通常试图通过接口表达类型层次结构。在Go中,更自然的是在接口和实现之间有清晰的分离。
值
为什么Go不提供隐式数字转换?
C中数值类型之间自动转换的便利性被它引起的混淆所抵消。表达式什么时候是无符号的?值有多大?它会溢出吗?结果是可移植的吗,独立于执行的机器? 它还使编译器复杂化;C的“通常算术转换”不容易实现,并且在不同架构上不一致。出于可移植性的原因,我们决定以代码中一些显式转换为代价,使事情变得清晰和直接。 Go中常量的定义——无符号和大小注解的任意精度值——大大改善了这种情况。
一个相关的细节是,与C不同,即使int是64位类型,int和int64也是不同的类型。int类型是通用的;如果你关心整数持有多少位,Go鼓励你明确指定。
Go中的常量是如何工作的?
尽管Go对不同类型的变量之间的转换很严格,但语言中的常量要灵活得多。
文字常量如23、3.14159和math.Pi占据一种理想数字空间,具有任意精度,没有溢出或下溢。
例如,math.Pi的值在源代码中指定为63位小数,涉及该值的常量表达式保持的精度超过float64能容纳的。
只有当常量或常量表达式被分配给变量——程序中的一个内存位置时,它才成为一个“计算机”数字,具有通常的浮点属性和精度。
此外, 因为它们是数字,不是类型化的值,Go中的常量可以比变量更自由地使用,从而缓解了严格转换规则的一些尴尬。 人们可以编写表达式,如
sqrt2 := math.Sqrt(2)而编译器不会抱怨,因为理想数字2可以安全准确地转换为float64以供math.Sqrt调用。
一篇题为常量的博客文章更详细地探讨了这个主题。
为什么映射是内置的?
与字符串相同的原因:它们是如此强大且重要的数据结构,提供一个优秀的实现并带有语法支持使编程更愉快。 我们相信Go的映射实现足够强大,可以满足绝大多数用途。 如果特定应用程序可以从自定义实现中受益,可以编写一个,但语法上不会那么方便;这似乎是一个合理的权衡。
为什么映射不允许切片作为键?
映射查找需要相等操作符,而切片不实现相等。 它们不实现相等,因为相等在这样的类型上没有明确定义;有多个考虑因素,涉及浅比较与深比较、指针与值比较、如何处理递归类型等。 我们可能会重新审视这个问题——实现切片的相等不会使任何现有程序无效——但如果没有一个关于切片相等应该意味着什么的明确想法,现在排除它是更简单的。
相等是为结构体和数组定义的,所以它们可以用作映射键。
为什么映射、切片和通道是引用,而数组是值?
这个话题有很多历史。 早期,映射和通道在语法上是指针,不可能声明或使用非指针实例。 此外,我们努力研究数组应该如何工作。 最终我们决定,指针和值的严格分离使语言更难使用。 改变这些类型,使其作为对关联的共享数据结构的引用,解决了这些问题。 这个改变给语言增加了一些令人遗憾的复杂性,但对可用性有巨大影响:Go成为一个更有生产力、更舒适的语言。
编写代码
库是如何文档化的?
为了从命令行访问文档,go 工具有一个 doc 子命令,提供对声明、文件、包等文档的文本接口。
全局包发现页面 pkg.go.dev/pkg/ 运行一个服务器,从网上的任何Go源代码中提取包文档,并将其作为HTML提供服务,链接到声明和相关元素。这是了解现有Go库的最简单方法。
在项目早期,有一个类似的程序 godoc,也可以运行以提取本地机器上文件的文档;pkg.go.dev/pkg/ 本质上是其后代。另一个后代是 pkgsite 命令,与 godoc 一样,可以本地运行,尽管它尚未集成到 go doc 显示的结果中。
有Go编程风格指南吗?
虽然没有明确的风格指南,但确实存在一种可识别的“Go风格”。
Go已经建立了围绕命名、布局和文件组织的惯例,以指导决策。
文档Effective Go包含关于这些主题的一些建议。
更直接的是,程序gofmt是一个美化器,其目的是执行布局规则;它取代了通常的解释性的是非规则。
仓库中的所有Go代码,以及开源世界中的绝大多数代码,都经过了gofmt的处理。
标题为Go代码审查评论的文档是许多关于Go惯用语细节的简短文章,这些细节程序员经常错过。 对于进行Go项目代码审查的人来说,这是一个方便的参考。
如何向Go库提交补丁?
库源代码位于仓库的src目录中。
如果你想做一个重大更改,请在开始之前先在邮件列表上讨论。
有关如何进行的信息,请参阅文档贡献给Go项目。
为什么“go get”在克隆仓库时使用HTTPS?
公司通常只允许在标准TCP端口80(HTTP)和443(HTTPS)上进行出站流量,阻止其他端口上的出站流量,包括TCP端口9418(git)和TCP端口22(SSH)。
当使用HTTPS而不是HTTP时,git默认强制执行证书验证,提供对中间人、窃听和篡改攻击的保护。
因此,go get命令出于安全考虑使用HTTPS。
Git可以配置为通过HTTPS进行身份验证,或使用SSH代替HTTPS。
要通过HTTPS进行身份验证,你可以在git查询的$HOME/.netrc文件中添加一行:
machine github.com login *用户名* password *API密钥*对于GitHub账户,密码可以是个人访问令牌。
Git还可以配置为对匹配给定前缀的URL使用SSH代替HTTPS。
例如,要对所有GitHub访问使用SSH,
在你的~/.gitconfig中添加这些行:
[url "ssh://git@github.com/"]
insteadOf = https://github.com/当使用私有模块,但对依赖项使用公共模块代理时,你可能需要设置GOPRIVATE。
有关详细信息和附加设置,请参阅私有模块。
我应该如何用“go get”管理包版本?
Go工具链有一个内置系统,用于管理相关包的版本集,称为模块。 模块在Go 1.11中引入,自1.14以来已准备好用于生产。
要创建一个使用模块的项目,运行go mod init。
此命令创建一个跟踪依赖项版本的go.mod文件。
go mod init example/project要添加、升级或降级依赖项,运行go get:
go get golang.org/x/text@v0.3.5有关入门的更多信息,请参阅教程:创建模块。
有关使用模块管理依赖项的指南,请参阅开发模块。
模块中的包应随着演变保持向后兼容性,遵循导入兼容性规则:
如果旧包和新包有相同的导入路径,
新包必须与旧包向后兼容。
Go 1兼容性指南在这里是一个很好的参考: 不要删除导出的名称,鼓励标记的复合字面量等。 如果需要不同的功能,添加一个新名称,而不是更改旧名称。
模块通过语义版本控制和语义导入版本控制将其编纂。
如果需要破坏兼容性,以新的主要版本发布模块。
主要版本2及以上的模块需要在其路径中包含一个主要版本后缀(如/v2)。
这保留了导入兼容性规则:模块不同主要版本的包具有不同的路径。
指针和分配
函数参数什么时候按值传递?
与C系列中的所有语言一样,Go中的一切都是按值传递的。
也就是说,函数总是获得被传递事物的副本,就像有一个赋值语句将值赋给参数一样。
例如,将int值传递给函数会复制int,传递指针值会复制指针,但不会复制它指向的数据。
(关于这对方法接收器的影响的讨论,请参见后续部分。)
映射和切片值的行为类似于指针:它们是指包含指向底层映射或切片数据指针的描述符。 复制映射或切片值不会复制它指向的数据。 复制接口值会复制存储在接口值中的事物。 如果接口值包含结构体,复制接口值会复制结构体。 如果接口值包含指针,复制接口值会复制指针,但不会复制它指向的数据。
注意,这个讨论是关于操作的语义。 实际实现可能会应用优化以避免复制,只要这些优化不改变语义。
什么时候应该使用指向接口的指针?
几乎从不。指向接口值的指针只在罕见、复杂的情况下出现,这些情况涉及延迟评估时隐藏接口值的类型。
将指向接口值的指针传递给期望接口的函数是一个常见错误。编译器会抱怨这个错误,但情况可能仍然令人困惑,因为有时指针是满足接口所必需的。 关键是要认识到,尽管指向具体类型的指针可以满足接口,但有一个例外指向接口的指针永远不能满足接口。
考虑变量声明,
var w io.Writer打印函数fmt.Fprintf将其第一个参数作为满足io.Writer的值——实现规范Write方法的东西。因此我们可以写
fmt.Fprintf(w, "hello, world\n")然而,如果我们传递w的地址,程序将无法编译。
fmt.Fprintf(&w, "hello, world\n") // 编译时错误。唯一的例外是,任何值,甚至是指向接口的指针,都可以赋值给空接口类型(interface{})的变量。
即便如此,如果值是指向接口的指针,这几乎肯定是一个错误;结果可能令人困惑。
我应该定义值上的方法还是指针上的方法?
func (s *MyStruct) pointerMethod() { } // 指针上的方法
func (s MyStruct) valueMethod() { } // 值上的方法对于不习惯指针的程序员来说,这两个例子之间的区别可能会令人困惑,但实际上情况非常简单。
在定义类型上的方法时,接收器(上面例子中的s)的行为就像它是方法的参数一样。
因此,将接收器定义为值还是指针,与函数参数应该是值还是指针是同一个问题。有几个考虑因素。
首先,也是最重要的,方法是否需要修改接收器?
如果需要,接收器必须是指针。
(切片和映射作为引用,所以它们的故事有点微妙,但例如,要更改方法中切片的长度,接收器仍然必须是指针。)
在上面的例子中,如果pointerMethod修改了s的字段,调用者会看到这些更改,但valueMethod是用调用者参数的副本调用的(这就是传递值的定义),所以它所做的更改对调用者不可见。
顺便说一下,在Java中,方法接收器一直是隐式的指针,尽管它们的指针性质有些被掩盖了(最近的进展正在为Java带来值接收器)。 Go中的值接收器是不寻常的。
第二是效率的考虑。如果接收器很大,比如一个大的struct,使用指针接收器可能更便宜。
接下来是一致性。如果类型的一些方法必须有指针接收器,其余的方法也应该有,所以无论如何使用类型,方法集都是一致的。 详见方法集部分。
对于基本类型、切片和小的structs这样的类型,值接收器非常便宜,因此除非方法的语义需要指针,否则值接收器是高效且清晰的。
new和make有什么区别?
简而言之:new分配内存,而make初始化切片、映射和通道类型。
更多细节请参见Effective Go的相关部分。
在64位机器上int的大小是多少?
int和uint的大小是特定于实现的,但在给定平台上它们是相同的。
为了可移植性,依赖于特定值大小的代码应该使用显式大小的类型,如int64。
在32位机器上,编译器默认使用32位整数,而在64位机器上,整数有64位。
(历史上,这并不总是正确的。)
另一方面,浮点标量和复杂类型总是有大小的(没有float或complex基本类型),因为程序员在使用浮点数时应该意识到精度。
(未类型化的)浮点常量的默认类型是float64。
因此foo := 3.0声明了一个float64类型的变量foo。
对于由(未类型化的)常量初始化的float32变量,变量类型必须在变量声明中明确指定:
var foo float32 = 3.0或者,常量必须通过转换给定类型,如foo := float32(3.0)。
我怎么知道变量是分配在堆上还是栈上?
从正确性的角度来看,你不需要知道。 Go中的每个变量只要还有对它的引用就存在。 实现选择的存储位置与语言的语义无关。
存储位置确实对编写高效的程序有影响。 当可能时,Go编译器会将函数的局部变量分配在该函数的栈帧中。 但是,如果编译器无法证明函数返回后变量不再被引用,那么编译器必须在垃圾回收的堆上分配变量,以避免悬空指针错误。 此外,如果局部变量非常大,将其存储在堆上而不是栈上可能更有意义。
在当前编译器中,如果变量的地址被取走,该变量就是堆上分配的候选。 但是,基本的逃逸分析能识别出一些情况,即这些变量在函数返回后将不再存活,可以驻留在栈上。
为什么我的Go进程使用这么多虚拟内存?
Go内存分配器为分配保留了一大块虚拟内存作为竞技场。 这个虚拟内存是特定Go进程本地的;保留不会剥夺其他进程的内存。
要查找分配给Go进程的实际内存量,使用Unix的top命令并查看RES (Linux) 或
RSIZE (macOS) 列。
待办事项: 查找这在Windows上是如何工作的。
并发
哪些操作是原子的?互斥锁呢?
Go中操作的原子性描述可以在Go内存模型文档中找到。
低级同步和原子原语在sync和sync/atomic包中可用。 这些包适用于简单的任务,如增加引用计数或保证小规模互斥。
对于更高级别的操作,如并发服务器之间的协调,更高级的技术可以带来更好的程序,Go通过其goroutines和channels支持这种方法。 例如,你可以构建程序,使每次只有一个goroutine负责特定数据。 这种方法由原始的Go谚语总结,
不要通过共享内存来通信。相反,通过通信来共享内存。
有关这个概念的详细讨论,请参见通过通信共享内存代码漫步及其相关文章。
大型并发程序可能会从这两个工具包中借用。
为什么我的程序在增加CPU数量后运行速度没有提升?
程序是否在增加CPU数量后运行得更快,取决于它解决的问题。 Go语言提供了并发原语,如goroutines和channels,但只有当底层问题本质上是并行的时候,并发才能实现并行性。 本质上是顺序的问题无法通过增加更多CPU来加速,而那些可以分解为可以并行执行的片段的问题可以加速,有时甚至是显著加速。
有时增加更多CPU可能会使程序变慢。 实际上,如果程序在同步或通信上花费的时间比做有用计算的时间多,使用多个OS线程时可能会经历性能下降。 这是因为在线程之间传递数据涉及切换上下文,这有显著成本,而这个成本可能会随着更多CPU的增加而增加。 例如,Go规范中的素数筛例子尽管启动了多个goroutines,但没有显著的并行性;增加线程(CPU)数量更可能使其变慢而不是变快。
有关这个主题的更多细节,请参见题为并发不是并行的演讲。
我如何控制CPU的数量?
同时执行goroutines可用的CPU数量由GOMAXPROCS shell环境变量控制,其默认值是可用CPU核心的数量。
因此,具有并行执行潜力的程序在多CPU机器上默认应该实现并行。
要更改要使用的并行CPU数量,设置环境变量或使用运行时包中同名函数来配置运行时支持以使用不同数量的线程。
将其设置为1将消除真正的并行性可能性,迫使独立的goroutines轮流执行。
运行时可以分配比GOMAXPROCS值更多的线程来服务多个未完成的I/O请求。
GOMAXPROCS只影响实际同时执行的goroutines数量;任意更多的goroutines可能在系统调用中被阻塞。
Go的goroutine调度器在平衡goroutines和线程方面做得很好,甚至可以抢占goroutine的执行,以确保同一线程上的其他goroutines不会饿死。
然而,它并不完美。
如果你看到性能问题,基于每个应用程序设置GOMAXPROCS可能会有所帮助。
为什么没有goroutine ID?
Goroutines没有名字;它们只是匿名的工作者。
它们不向程序员暴露任何唯一标识符、名称或数据结构。
有些人对此感到惊讶,期望go语句返回一些可以用来稍后访问和控制goroutine的项目。
Goroutines匿名的根本原因是为了在编写并发代码时可以使用完整的Go语言。 相比之下,当线程和goroutines被命名时,使用模式会限制使用它们的库能做什么。
这里是困难之处的说明。
一旦一个人给一个goroutine命名并围绕它构建模型,它就变得特殊,一个人会倾向于将所有计算与那个goroutine关联,忽略使用多个可能共享的goroutines进行处理的的可能性。
如果net/http包将每个请求的状态与一个goroutine关联,客户端将不能在使用请求时启动更多goroutines。
此外,对于图形系统库这样的库的经验表明,要求所有处理都在“主线程”上发生,在并发语言中部署时这种方法可能多么尴尬和限制性。 特殊线程或goroutine的存在迫使程序员扭曲程序以避免意外操作错误线程导致的崩溃和其他问题。
对于那些特定goroutine确实特殊的案例,语言提供了诸如channels这样的特性,可以以灵活的方式与之交互。
函数和方法
为什么T和*T有不同的方法集?
正如Go规范所说,类型T的方法集由所有接收器类型为T的方法组成,而相应指针类型*T的方法集由所有接收器为*T或T的方法组成。
这意味着*T的方法集包括T的方法集,但不是反过来。
这种区别出现是因为如果接口值包含指针*T,方法调用可以通过解引用指针获得值,但如果接口值包含值T,方法调用没有安全的方式获得指针。
(这样做将允许方法修改接口内值的内容,这是语言规范不允许的。)
即使在编译器可以取值的地址以传递给方法的情况下,如果方法修改值,更改将在调用者中丢失。
作为例子,如果下面的代码是有效的:
var buf bytes.Buffer
io.Copy(buf, os.Stdin)它会将标准输入复制到buf的副本中,而不是buf本身。
这几乎从来不是期望的行为,因此被语言禁止。
闭包作为goroutines运行时会发生什么?
由于循环变量的工作方式,在Go 1.22版本之前(有关更新,请参见本节末尾),使用闭包和并发时可能会引起一些困惑。考虑以下程序:
package main
import "fmt"
func main() {
done := make(chan bool)
values := []string{"a", "b", "c"}
for _, v := range values {
go func() {
fmt.Println(v)
done <- true
}()
}
// 等待所有goroutines完成再退出
for range values {
<-done
}
}人们可能会错误地期望看到a, b, c作为输出。
你可能看到的却是c, c, c。
这是因为循环的每次迭代都使用变量v的同一实例,所以每个闭包共享那个单一变量。当闭包运行时,它打印fmt.Println执行时v的值,但v可能自goroutine启动以来已被修改。为了在问题发生前帮助检测这些问题,运行go vet。
为了在启动每个闭包时绑定v的当前值,必须修改内部循环以在每次迭代中创建新变量。
一种方法是将变量作为参数传递给闭包:
for _, v := range values {
go func(u string) {
fmt.Println(u)
done <- true
}(v)
}在这个例子中,v的值作为参数传递给匿名函数。然后该值在函数内部作为变量u可访问。
更简单的方法是只需创建一个新变量,使用一种在Go中可能看起来奇怪但完全有效的声明风格:
for _, v := range values {
v := v // 创建一个新的'v'。
go func() {
fmt.Println(v)
done <- true
}()
}这种语言行为,不为每次迭代定义新变量,后来被视作一个错误,并在Go 1.22中得到解决,它确实为每次迭代创建新变量,消除了这个问题。
控制流
为什么Go没有?:操作符?
Go中没有三元测试操作。你可以使用以下代码来达到相同的结果:
if expr {
n = trueVal
} else {
n = falseVal
}Go中缺少?:操作符的原因是,语言的设计者看到这种操作被过于频繁地使用,以创建难以理解的复杂表达式。
if-else形式虽然更长,但无疑是更清晰的。
一种语言只需要一个条件控制流构造。
类型参数
为什么Go有类型参数?
类型参数允许所谓的泛型编程,其中函数和数据结构以类型为参数进行定义,这些类型在函数和数据结构被使用时才具体指定。 例如,它们使得编写一个返回任何有序类型两个值中最小值的函数成为可能,而无需为每种可能类型编写单独的版本。 有关更深入的解释和示例,请参见博客文章为什么需要泛型?。
Go中的泛型是如何实现的?
编译器可以选择是单独编译每个实例化,还是将相似的实例化为一个单一实现进行编译。 单一实现方法类似于具有接口参数的函数。 不同的编译器会对不同的情况做出不同的选择。 标准的Go编译器通常为每个具有相同形状的类型参数发出一个实例化,其中形状由类型的大小和包含的指针位置等属性决定。 未来的版本可能会在编译时间、运行时效率和代码大小之间进行权衡实验。
Go中的泛型与其他语言中的泛型相比如何?
所有语言中的基本功能是相似的:可以使用稍后指定的类型来编写类型和函数。 话虽如此,还是有一些差异。
-
Java
在Java中,编译器在编译时检查泛型类型,但在运行时删除类型。 这被称为类型擦除。 例如,在编译时被称为
List<Integer>的Java类型在运行时将变成非泛型类型List。 这意味着,例如,在使用Java形式的反射时,不可能区分List<Integer>类型的值和List<Float>类型的值。 在Go中,泛型类型的反射信息包括完整的编译时类型信息。Java使用类型通配符,如
List<? extends Number>或List<? super Number>来实现泛型协变和逆变。 Go没有这些概念,这使得Go中的泛型类型简单得多。 -
C++
传统上,C++模板不强制对类型参数施加任何约束,尽管C++20通过概念支持可选约束。 在Go中,约束对于所有类型参数都是强制的。 C20概念表示为必须与类型参数一起编译的小代码片段。 Go约束是定义所有允许类型参数集合的接口类型。
C++支持模板元编程;Go不支持。 实际上,所有C++编译器都在实例化点编译每个模板;如上所述,Go可以并且确实对不同实例化使用不同的方法。
-
Rust
Rust版本的约束被称为trait bounds。 在Rust中,trait bound和类型之间的关联必须显式定义,要么在定义trait bound的crate中,要么在定义类型的crate中。 在Go中,类型参数隐式满足约束,就像Go类型隐式实现接口类型一样。 Rust标准库为标准操作(如比较或加法)定义了标准traits;Go标准库没有,因为这些可以通过接口类型在用户代码中表示。唯一的例外是Go的
comparable预定义接口,它捕获了类型系统中无法表示的属性。 -
Python
Python不是一种静态类型语言,所以可以说所有Python函数默认都是泛型的:它们总是可以用任何类型的值调用,任何类型错误都在运行时检测。
为什么Go在类型参数列表中使用方括号?
Java和C++在类型参数列表中使用尖括号,如Java的List<Integer>和C++的std::vector<int>。
然而,这个选项对Go来说不可用,因为它导致了一个语法问题:当解析函数内的代码时,例如v := F<T>,在看到<的那一刻,我们无法确定是看到实例化还是使用<操作符的表达式。
没有类型信息,这非常难以解决。
例如,考虑这样的语句
a, b = w < x, y > (z)没有类型信息,不可能决定赋值的右边是一对表达式(w < x and y > z),还是一个返回两个结果的泛型函数实例化和调用((w<x, y>)(z))。
Go的一个关键设计决策是解析可以在没有类型信息的情况下进行,这在泛型中使用尖括号时似乎是不可能的。
Go在使用方括号方面不是唯一或原创的;还有其他语言如Scala也在泛型代码中使用方括号。
为什么Go不支持具有类型参数的方法?
Go允许泛型类型具有方法,但除了接收器之外,这些方法的参数不能使用参数化类型。 我们不预期Go会添加泛型方法。
问题在于如何实现它们。
具体来说,考虑检查接口中的值是否实现了另一个具有额外方法的接口。
例如,考虑这个类型,一个空结构体,具有一个泛型Nop方法,返回其参数,对于任何可能的类型:
type Empty struct{}
func (Empty) Nop[T any](x T) T {
return x
}现在假设一个Empty值存储在any中并传递给其他代码,检查它能做什么:
func TryNops(x any) {
if x, ok := x.(interface{ Nop(string) string }); ok {
fmt.Printf("string %s\n", x.Nop("hello"))
}
if x, ok := x.(interface{ Nop(int) int }); ok {
fmt.Printf("int %d\n", x.Nop(42))
}
if x, ok := x.(interface{ Nop(io.Reader) io.Reader }); ok {
data, err := io.ReadAll(x.Nop(strings.NewReader("hello world")))
fmt.Printf("reader %q %v\n", data, err)
}
}如果x是Empty,这段代码如何工作?
似乎x必须满足所有三个测试,以及任何其他类型的其他形式。
调用这些方法时运行什么代码? 对于非泛型方法,编译器为所有方法实现生成代码并将它们链接到最终程序中。 但对于泛型方法,可以有无限多的方法实现,所以需要不同的策略。
有四个选择:
-
在链接时,列出所有可能的动态接口检查,然后查找满足这些检查但缺少已编译方法的类型,然后重新调用编译器以添加这些方法。
这将使构建显著变慢,需要在链接后停止并重复一些编译。它会特别减慢增量构建。更糟的是,新编译的方法代码本身可能有新的动态接口检查,这个过程必须重复。可以构建例子,其中这个过程永远不会结束。
-
实现某种JIT,在运行时编译所需的方法代码。
Go从纯粹提前编译的简单性和可预测性能中获益匪浅。 我们不愿意仅仅为了实现一个语言特性而承担JIT的复杂性。
-
安排为每个泛型方法发出一个慢速回退,使用类型参数的每个可能语言操作的函数表,然后对动态测试使用那个回退实现。
这种方法会使由意外类型参数化的泛型方法比由编译时观察到的类型参数化的相同方法慢得多。 这将使性能变得非常不可预测。
-
定义泛型方法根本不能用于满足接口。
接口是Go编程的重要组成部分。 不允许泛型方法满足接口从设计角度来看是不可接受的。
这些选择都不好,所以我们选择了“以上皆非”。
代替具有类型参数的方法,使用具有类型参数的全局函数,或将类型参数添加到接收器类型。
有关更多详细信息,包括更多示例,请参见[提案](https://go.dev/design/43651-type-parameters#no-parameterized-methods
为什么我不能为参数化类型的接收器使用更具体的类型?
泛型类型的方法声明使用包含类型参数名称的接收器。
也许是因为在调用点指定类型的语法相似性,
有些人认为这提供了一种机制,通过命名接收器中的特定类型(如string)来为某些类型参数生成定制的方法:
type S[T any] struct { f T }
func (s S[string]) Add(t string) string {
return s.f + t
}这失败了,因为单词string被编译器视为方法中类型参数的名称。
编译器错误消息将是类似“operator + not defined on s.f (variable of type string)”。
这可能会令人困惑,因为+操作符在预声明类型string上工作得很好,
但声明已经为这个方法覆盖了string的定义,
而操作符在那个不相关的string版本上不工作。
像这样覆盖预声明名称是有效的,但这是很奇怪的做法,通常是一个错误。
为什么编译器不能推断我程序中的类型参数?
有很多情况下,程序员很容易看到泛型类型或函数的类型参数应该是什么,但语言不允许编译器推断它。 类型推断被有意限制,以确保永远不会对推断的类型产生任何混淆。 其他语言的经验表明,意外的类型推断在阅读和调试程序时会导致相当大的混淆。 总是可以指定调用中要使用的显式类型参数。 将来可能会支持新的推断形式,只要规则保持简单和清晰。
包和测试
我如何创建多文件包?
将包的所有源文件放在一个目录中。 源文件可以自由引用不同文件中的项目;不需要前向声明或头文件。
除了被分成多个文件外,该包将像单文件包一样编译和测试。
我如何编写单元测试?
在与包源文件相同的目录中创建一个以_test.go结尾的新文件。
在该文件中,import "testing"并编写形式如下的函数
func TestFoo(t *testing.T) {
...
}在该目录中运行go test。
该脚本找到Test函数,
构建测试二进制文件并运行它。
更多细节请参见如何编写Go代码文档,
testing包
和go test子命令。
我最喜欢的测试辅助函数在哪里?
Go的标准testing包使得编写单元测试变得容易,但它缺少其他语言的测试框架提供的功能,如断言函数。
本文档的早期部分解释了为什么Go没有断言,
同样的论点也适用于在测试中使用assert。
适当的错误处理意味着在一个测试失败后让其他测试继续运行,
这样调试失败的人就能得到问题的完整画面。对于测试来说,报告isPrime对2、3、5和7(或对2、4、8和16)给出了错误的答案,比报告isPrime对2给出了错误的答案并且因此没有运行更多测试更有用。触发测试失败的程序员可能不熟悉失败的代码。现在投入时间编写一个好的错误消息,以后当测试失败时会得到回报。
相关的一点是,测试框架往往会发展成自己的迷你语言,带有条件和控制以及打印机制, 但Go已经有了所有这些功能;为什么要重新创建它们? 我们更愿意用Go编写测试;少学一种语言,这种方法使测试保持简单易懂。
如果编写好错误所需的额外代码量看起来重复且压倒性,如果测试是表驱动的,迭代数据结构的输入和输出列表,测试可能会工作得更好(Go对数据结构字面量有极好的支持)。然后,编写好的测试和好的错误消息的工作将在许多测试用例上分摊。标准Go库充满了说明性示例,如
fmt包的格式化测试。
为什么X不在标准库中?
标准库的目的是支持运行时库,连接到操作系统,并提供许多Go程序所需的关键功能,如格式化I/O和网络。 它还包含对Web编程重要的元素,包括密码学和HTTP、JSON及XML等标准的支持。
没有明确的定义标准库中包含什么内容的标准,因为长期以来,这是唯一的Go库。 然而,有定义今天添加内容的标准。
标准库的新增内容很少,且包含的门槛很高。 标准库中包含的代码承担着巨大的持续维护成本(通常由原始作者以外的人承担), 受Go 1兼容性承诺约束(阻止修复API中的任何缺陷), 并受Go发布计划约束, 阻止用户快速获得错误修复。
大多数新代码应位于标准库之外,并通过go工具的
go get命令访问。
这样的代码可以有自己的维护者、发布周期和兼容性保证。
用户可以在pkg.go.dev找到包并阅读其文档。
尽管标准库中有一些部分并不真正属于那里,如log/syslog,但由于Go 1兼容性承诺,我们继续维护库中的所有东西。
但我们鼓励大多数新代码放在其他地方。
实现
构建编译器使用了什么编译器技术?
Go有几个生产编译器,还有几个为各种平台开发中的其他编译器。
默认编译器gc包含在Go发行版中,作为go命令支持的一部分。
Gc最初用C编写,因为自举的困难——你需要一个Go编译器来设置Go环境。
但情况已经进步,自Go 1.5发布以来,编译器已经是一个Go程序。
编译器从C转换为Go使用了自动翻译工具,如这篇设计文档
和演讲中所述。
因此,编译器现在是“自托管的”,这意味着我们需要面对自举问题。
解决方案是已经有了一个工作的Go安装,就像通常有了一个工作的C安装一样。
如何从源代码启动新Go环境的描述在这里和
这里。
Gc用Go编写,使用递归下降解析器,并使用自定义加载器(也用Go编写,但基于Plan 9加载器)生成ELF/Mach-O/PE二进制文件。
Gccgo编译器是一个用C++编写的递归下降解析器前端,与标准GCC后端耦合。一个实验性的
LLVM后端使用相同的前端。
在项目开始时,我们考虑过为gc使用LLVM,但决定它太大太慢,无法满足我们的性能目标。
回想起来更重要的是,从LLVM开始会使引入一些ABI和相关更改(如Go需要但不属于标准C设置的栈管理)变得更加困难。
Go最终被证明是一种实现Go编译器的优秀语言,尽管这不是它的最初目标。 从一开始就不自托管使Go的设计可以专注于其原始用例,即网络服务器。 如果我们决定Go应该尽早编译自己,我们可能会最终得到一个更针对编译器构建的语言,这是一个有价值的目标,但不是我们最初的。
尽管gc有自己的实现,但原生的词法分析器和解析器在go/parser包中可用,还有一个原生的类型检查器。
gc编译器使用这些库的变体。
运行时支持是如何实现的?
再次由于自举问题,运行时代码最初主要用C编写(带有一小部分汇编器),但后来已转换为Go(除了一些汇编器部分)。
Gccgo的运行时支持使用glibc。
gccgo编译器使用一种称为分段栈的技术实现goroutines,
这由对gold链接器的最新修改支持。
Gollvm类似地构建在相应的LLVM基础设施上。
为什么我的简单程序生成的二进制文件这么大?
gc工具链中的链接器默认创建静态链接的二进制文件。
因此,所有Go二进制文件都包含Go运行时,以及支持动态类型检查、反射甚至恐慌时堆栈跟踪所需的运行时类型信息。
一个简单的C "hello, world"程序在Linux上使用gcc静态编译和链接后大约750 kB,包括printf的实现。
一个等效的Go程序使用fmt.Printf重达几兆字节,但这包含了更强大的运行时支持和类型及调试信息。
使用gc编译的Go程序可以使用-ldflags=-w标志链接以禁用DWARF生成,
从二进制文件中移除调试信息,但不会损失其他功能。
这可以显著减小二进制文件的大小。
我可以停止关于未使用变量/导入的抱怨吗?
未使用变量的存在可能表明有错误,而未使用的导入只会减慢编译速度, 这种影响会随着程序随时间积累代码和程序员而变得显著。 出于这些原因,Go拒绝编译具有未使用变量或导入的程序, 以短期便利换取长期构建速度和程序清晰度。
尽管如此,在开发代码时,通常会暂时创建这些情况, 在程序编译之前必须编辑掉它们可能会很烦人。
有些人要求编译器选项来关闭这些检查,或至少将它们减少为警告。 然而,这样的选项尚未添加, 因为编译器选项不应影响语言的语义,而且Go编译器不报告警告,只报告阻止编译的错误。
没有警告有两个原因。首先,如果值得抱怨,就值得在代码中修复。(相反,如果不值得修复,就不值得提及。)其次,让编译器生成警告会鼓励实现对弱情况发出警告,这会使编译变得嘈杂,掩盖了应该修复的真正错误。
不过,很容易解决这种情况。使用空白标识符来让未使用的内容在你开发时持续存在。
import "unused"
// 此声明通过引用包中的项目来标记导入为已使用。
var _ = unused.Item // TODO: 提交前删除!
func main() {
debugData := debug.Profile()
_ = debugData // 仅在调试时使用。
....
}如今,大多数Go程序员使用一个工具,
goimports,
它会自动重写Go源文件以拥有正确的导入,
实际上消除了未使用导入的问题。
这个程序很容易连接到大多数编辑器和IDE,以在Go源文件被写入时自动运行。
此功能也内置在gopls中,如上面讨论的。
为什么我的病毒扫描软件认为我的Go发行版或编译的二进制文件被感染了?
这在Windows机器上很常见,几乎总是误报。 商业病毒扫描程序经常被Go二进制文件的结构搞糊涂, 它们不像其他语言编译的二进制文件那样常见。
如果你刚安装Go发行版,系统报告它被感染了,那肯定是个错误。 为了彻底,你可以通过将校验和与下载页面上的校验和进行比较来验证下载。
在任何情况下,如果你认为报告是错误的,请向你的病毒扫描器供应商报告一个错误。 也许随着时间的推移,病毒扫描器可以学会理解Go程序。
性能
为什么Go在基准测试X上表现不佳?
Go的设计目标之一是接近C在可比程序上的性能,但在一些基准测试上表现相当差,包括 golang.org/x/exp/shootout中的几个。 最慢的依赖于Go中没有可比性能版本的库。 例如,pidigits.go 依赖于多精度数学包,C版本使用GMP(用优化的汇编器编写),而Go的版本不使用。 依赖于正则表达式的基准测试 (例如regex-dna.go) 本质上是在将Go的原生regexp包与成熟的、高度优化的正则表达式库如PCRE进行比较。
基准测试游戏通过广泛的调优获胜,大多数基准测试的Go版本需要关注。 如果你测量真正可比的C和Go程序 (reverse-complement.go 是一个例子),你会发现这两种语言在原始性能上比这个套件表明的更接近。
尽管如此,仍有改进的空间。编译器很好但可以更好,许多库需要主要的性能工作,垃圾收集器还不够快。(即使它是,注意不要产生不必要的垃圾也会产生巨大影响。)
在任何情况下,Go通常可以非常有竞争力。 随着语言和工具的发展,许多程序的性能有了显著改善。 请参阅关于分析Go程序的博客文章,了解一个信息丰富的例子。 它很老了,但仍然包含有用的信息。
与C的差异
为什么语法与C如此不同?
除了声明语法外,差异并不大,源于两个愿望。 首先,语法应该感觉轻松,没有太多强制性关键字、重复或奥秘。 其次,语言被设计为易于分析,可以在没有符号表的情况下解析。 这使得构建调试器、依赖分析器、自动文档提取器、IDE插件等工具变得更加容易。 C及其后代在这方面是出了名的困难。
为什么声明是反向的?
如果你习惯了C,它们只是反向的。在C中,概念是变量应该像表示其类型的表达式一样声明,这是一个好主意,但类型和表达式语法混合得不太好,结果可能令人困惑;考虑函数指针。
Go主要分离了表达式和类型语法,这简化了事情(使用前缀*表示指针是一个例外,证明了规则)。
在C中,声明
int* a, b;声明a为指针但不声明b;在Go中
var a, b *int声明两者都是指针。这更清晰、更规则。
此外,:=短声明形式认为完整的变量声明应该与:=呈现相同的顺序,因此
var a uint64 = 1与
a := uint64(1)有相同的效果。
通过拥有一个与表达式语法不同的类型语法,解析也得到了简化;关键字如func和chan使事情保持清晰。
有关更多详细信息,请参阅关于Go的声明语法
为什么没有指针算术?
出于安全考虑。没有指针算术,就有可能创建一种语言,它永远不会推导出一个非法地址并错误地成功。编译器和硬件技术已经进步到使用数组索引的循环可以和使用指针算术的循环一样高效的程度。此外,缺乏指针算术可以简化垃圾收集器的实现。
为什么 ++ 和 -- 是语句而不是表达式?而且为什么是后缀而不是前缀?
没有指针算术,前后缀递增操作符的便利性下降了。通过将它们完全从表达式层次结构中移除,表达式语法得到了简化,围绕 ++ 和 -- 求值顺序的混乱问题(考虑 f(i++) 和 p[i] = q[++i])也被消除了。这种简化是显著的。至于后缀与前缀,两者都可以很好地工作,但后缀版本更传统;对前缀的坚持随着STL(一个用于其名称中讽刺地包含后缀递增的语言的库)而出现。
为什么有大括号但没有分号?而且为什么我不能把开大括号放在下一行?
Go使用大括号进行语句分组,这是一种对使用过C家族中任何语言的程序员都熟悉的语法。然而,分号是为解析器而不是为人类准备的,我们希望尽可能消除它们。为了实现这个目标,Go从BCPL借了一个技巧:分隔语句的分号在正式语法中,但由词法分析器在任何可能是语句结束的行末自动注入,无需前瞻。这在实践中效果很好,但有一个效果是它强制了一种大括号风格。例如,函数的开大括号不能单独出现在一行上。
有些人认为词法分析器应该做前瞻,以允许大括号出现在下一行。我们不同意。由于Go代码旨在由gofmt自动格式化,某种风格必须被选择。那种风格可能与你习惯的C或Java不同,但Go是一种不同的语言,gofmt的风格和其他任何风格一样好。更重要的是——重要得多——为所有Go程序规定单一程序化格式的优势远远超过任何特定风格的感知劣势。还要注意,Go的风格意味着Go的交互式实现可以使用标准语法一次一行,无需特殊规则。
为什么需要垃圾回收?它不会太昂贵吗?
系统程序中最大的记账来源之一是管理分配对象的生命周期。 在像C这样手动完成的语言中,它会消耗程序员大量时间,并且经常是导致顽固错误的原因。 即使在像C++或Rust这样提供机制协助的语言中,这些机制也会对软件的设计产生重大影响,通常会增加自身的编程开销。 我们认为消除这种程序员开销至关重要,而且近年来垃圾收集技术的进步使我们相信它可以足够便宜地实现,并且延迟足够低,可以成为网络系统的可行方法。
并发编程的许多困难根源在于对象生命周期问题: 当对象在线程之间传递时,保证它们安全释放变得繁琐。 自动垃圾收集使得并发代码更容易编写。 当然,在并发环境中实现垃圾收集本身是一个挑战,但解决它一次而不是在每个程序中解决它有助于每个人。
最后,除了并发,垃圾收集使接口更简单,因为它们不需要指定内存如何跨它们管理。
这并不是说最近在像Rust这样的语言中为解决资源管理问题而带来的新想法的工作是错误的;我们鼓励这项工作,并对其发展感到兴奋。 但Go采取了更传统的方法,通过垃圾收集,且仅通过垃圾收集来解决对象生命周期问题。
当前实现是标记-清除收集器。 如果机器是多处理器,收集器在单独的CPU核心上并行于主程序运行。 近年来对收集器的主要工作已将暂停时间减少到通常亚毫秒范围,即使对于大堆也是如此,几乎消除了对网络服务器中垃圾收集的主要反对意见之一。 工作仍在继续以精炼算法,进一步减少开销和延迟,并探索新方法。 Go团队的Rick Hudson在2018年的ISMM主题演讲描述了迄今为止的进展,并建议了一些未来方法。
关于性能,请记住,Go为程序员提供了对内存布局和分配的相当多的控制,比典型的垃圾收集语言多得多。 细心的程序员可以通过很好地使用语言来显著减少垃圾收集开销; 请参阅关于分析Go程序的文章以获取一个工作示例, 包括Go分析工具的演示。