这讲要解决什么
- 能区分网络延迟、节点崩溃与部分失败
- 会用状态机和不变量描述协议
- 解释并发不是并行的同义词的核心问题
- 按协议顺序推演pipeline 与 fan-out/fan-in
- 评估工程取舍:轻量 goroutine 让并发表达简单,但若没有背压和取消,简单启动会演变成泄漏与资源失控。
Go 语法不是目标:目标是把协议的不变量安全地放进并发程序
分布式协议通常写成原子状态机:收到消息,检查当前任期和日志,完成一次状态迁移,发出新消息。真实 Go 程序却由多个 goroutine 并发运行,RPC 在解锁期间等待,timer 随时触发,旧回复可能很晚回来。实现的任务是让这些交错仍等价于某个合法状态机执行。
因此学习 Go 要围绕四个问题:谁拥有每个可变字段;哪些字段必须一起变化;阻塞操作前后哪些前提可能失效;每个后台 goroutine 如何退出。struct、方法、interface、slice、map、Mutex、Cond 和 channel 都是回答这些问题的工具,而不是独立语法点。
老师的 Go slides 和 FAQ 提供语言事实;本章把它们放进 6.5840 的实现语境。看到一个代码片段,不只问“会不会 data race”,还要问“即使没有 data race,是否可能把旧 term 的结果写进新 term”“是否持锁等待了只有别的 goroutine 才能产生的事件”“测试结束后循环是否泄漏”。
下面始终用一个简化 Raft 节点作贯穿对象:它有 currentTerm、role、log、commitIndex、条件变量和若干 RPC worker。你会先制定所有权表,再走一次解锁 RPC,最后设计可复现测试。
并发不是并行的同义词
goroutine 是独立执行的函数,运行时把大量 goroutine 多路复用到线程。并发用于组织多个可能交错的活动,并行才是同一时刻在多个核心执行。设计时先明确工作单元、阻塞点和取消路径,再决定开多少 goroutine;无限制地为每个输入创建 goroutine 会把负载峰值变成内存峰值。
pipeline 与 fan-out/fan-in
pipeline 把每个阶段表示成接收 channel、处理、再发送的 goroutine。fan-out 让多个 worker 消费同一队列,fan-in 合并结果。每个发送者都必须知道下游是否还在读取,否则泄漏在阻塞发送上;通常由创建 channel 的一方负责关闭,并用 WaitGroup 保证所有发送者退出后再 close。
context 与结构化生命周期
context 传递截止时间、取消信号和请求范围值。父请求结束后,所有派生 goroutine 应能沿 Done 通道退出;不要把 context 存进长期结构或用它承载可选业务参数。取消是协作式的,所以循环、RPC 和 channel 等待都要显式 select 取消分支。
接口、错误与测试
小接口让调用者定义所需能力,便于替换网络、时钟和存储实现。错误要保留语义链,调用者用 errors.Is/As 分类处理。并发测试除了功能结果,还应运行 race detector、使用可控时钟或注入阻塞点,验证退出、取消和慢消费者路径。
用类型、接口与所有权缩小并发表面积
Go 没有类层次,却用 struct、方法和隐式接口组合大型系统。接口由使用方描述所需最小方法,具体类型无需声明 implements;这让 RPC 传输、持久化、计时器和测试替身可以通过小接口解耦。分布式代码里接口越窄,越容易控制故障注入与替换实现。
值接收者得到接收者副本,指针接收者可修改共享对象;但指针并不等同于安全所有权。一个 *Raft 被多个 goroutine 引用后,任何可变字段都必须有明确同步规则。切片是包含指针、长度、容量的描述符,复制切片仍可能共享底层数组;map、channel 也都是引用式对象,函数参数按值传递不意味着深复制。
把可变状态聚合进一个 struct、用一把 mutex 保护相关不变量,通常比给每个字段一把锁更容易证明。读出需要跨阻塞调用使用的数据时,可在锁内复制快照,解锁后做 RPC;回来重新加锁并验证 term/role 等前提仍成立。不能假设解锁期间世界没有变化。
零值可用是 Go API 的重要习惯:sync.Mutex、sync.WaitGroup 可直接作为字段,无需额外初始化。相反,map 和 channel 使用前必须 make,nil channel 的发送/接收会永久阻塞,nil map 可读但写会 panic。把这些差异变成构造函数不变量,能减少实验里偶发死锁。
在写代码前完成一张状态所有权表
列出 currentTerm / votedFor / role / log / commitIndex / lastApplied / nextIndex[] / dead。对每个字段写:读写者、保护方式、必须同时成立的不变量、是否持久化。最简单可靠的起点是一个 mu 保护协议状态,原子标志只用于独立的停止信号;不要一开始就给每个字段分锁。
例如 votedFor 与 currentTerm 共同表达“每任期最多投一票”,二者切换任期时必须一起更新并持久化。commitIndex 不能超过本地最后日志 index,lastApplied 不能超过 commitIndex。这些是跨字段不变量,分别使用 atomic 读写无法阻止观察到从未存在过的组合。
slice 复制尤其危险。把 rf.log 赋给局部变量只复制 slice header,底层数组仍可能被其他 goroutine append 覆盖。发送 RPC 前应复制所需 entries;读取 map 也不能在解锁后继续持有可被并发修改的引用。接口边界要传输稳定快照,而不是把内部可变对象泄漏出去。
方法接收者选择也服务于此设计:会修改或共享大 struct 的方法用指针接收者;只读值接收者仍不自动线程安全,因为字段中的 slice/map 可能指向共享内存。类型系统告诉你形状,所有权表才告诉你并发规则。
happens-before:同步原语真正保证了什么
Mutex 的 Unlock 与随后成功的 Lock 建立 happens-before,使后者看到前者在临界区的写入。channel 的发送与对应接收也建立顺序;关闭 channel 与观察到关闭之间同样传递可见性。同步原语不仅防止两个 CPU 同时写,还约束编译器与处理器的内存重排。
sync.Cond 总与一把锁绑定。等待方必须在循环中检查条件:持锁判断不满足,调用 Wait;Wait 原子地解锁并睡眠,醒来前重新加锁。Signal/Broadcast 只是提示“条件也许改变”,不是把条件本身传给等待者;唤醒可能被别的 goroutine 抢先消费,所以不能用 if 代替 for。
defer mu.Unlock() 能让所有返回路径释放锁,适合短小 handler;在大循环或性能敏感路径中也要理解 defer 的作用域。最常见错误不是 defer 慢,而是在持锁函数内部调用另一个会尝试同一锁的函数,Go Mutex 不可重入,会自锁死。
原子操作适合单个计数或标志,但多个字段构成的不变量不能靠分别 atomic 读写维持。例如 currentTerm、votedFor 与持久日志需要一致更新;把它们拆成独立原子变量会产生从未存在过的组合快照。优先使用清晰的锁域,确有测量证据再细化。
把上面的机制落到消息、状态与失败路径中。
term=7, role=leader
世界可以继续变化
可能已经 term=8 follower
只有版本匹配才合并结果
解锁发 RPC 后,怎样证明迟到结果不会污染现在
持有全局锁执行 RPC 会阻塞其他 handler、timer 和 applier,甚至形成等待环,因此常见模式是:锁内读取状态并构造参数;记录逻辑版本;解锁发送;回复后重新加锁;重新验证版本和角色;条件仍成立才合并结果。
版本通常至少包含 term 和 role,日志复制还要包含目标 peer、请求对应的 prevLogIndex 或发送时 nextIndex。不能只检查 RPC 返回成功,因为成功描述的是远端处理当时参数的结果,不证明本地现在仍处于相同领导任期。若已经成为 follower,旧成功回复不应推进 matchIndex;若 nextIndex 已因另一回复前进,旧失败回复也不应把它倒退。
这是一种乐观并发控制:解锁期间允许世界变化,回来通过版本检查决定结果是否仍可提交。锁保护单个瞬间,版本保护跨越阻塞操作的逻辑区间。后面 FaRM 的 OCC 会把同一思想扩展到分布式对象版本。
条件变量用于等待状态谓词,而非等待一次通知。applier 在锁内循环检查 lastApplied < commitIndex;Wait 原子解锁并睡眠,醒来重新加锁后再检查。Signal 只是提示条件可能改变,真正依据仍是字段。用 if 会在多等待者或伪唤醒式交错下执行错误路径。
把并发错误变成可重放的证据
go test -race 通过运行时插桩寻找缺少同步的冲突访问,是第一道门槛,但它只覆盖本次执行触达的交错,也不理解协议语义。没有 race 不代表没有死锁、遗漏唤醒、过期回复或错误提交;有 race 即使测试结果正确也必须修复。
并发测试要增加重复、随机延迟和超时,并让失败日志包含单调逻辑信息:节点 ID、term、role、log index、request ID、旧值与新值。墙上时钟可辅助看延迟,却不能代替协议顺序。打印所有心跳会淹没关键事件,应围绕角色变化、持久状态更新、提交推进和拒绝原因记录。
不要把睡眠当同步。time.Sleep(10*time.Millisecond) 只是在当前机器上提高某种交错概率,负载或调度变化就失效;使用 channel、WaitGroup、Cond 或可观察状态等待具体条件。测试超时应报告仍在等待的条件和 goroutine 状态,而非仅返回“timeout”。
资源生命周期也属于正确性:time.Ticker 应停止,后台 goroutine 应观察 killed/done 信号,测试间不能继续发送旧 RPC。一次测试看似通过,泄漏 goroutine 却可能在下一次实例复用端口或对象时制造幽灵事件。可终止性与状态隔离应在设计时写入。
把并发 bug 变成可复现证据,而不是靠多打印几行
测试分三层。第一层用 go test -race 找缺少 happens-before 的内存访问;它发现 data race 很强,但无法证明协议正确。第二层用小型确定性单测检查 helper 与状态迁移,例如更高 term 回复一定降级、重复 apply 不发生。第三层用故障测试改变延迟、丢包、重排和崩溃,检查不变量在长时间交错中仍成立。
日志要结构化:时间只用于排序参考,核心字段是 node、term、role、event、peer、index 和 request ID。失败时先找到第一条不可能事件,例如同一 term 投两票、commitIndex 倒退、同一 index apply 两个值;最终超时往往只是早先错误的结果。
goroutine 生命周期也要进入测试。每个无限循环必须观察 done/kill 条件,timer 需要 stop/reset 规则,channel 发送不能让退出路径永久阻塞。反复运行测试后 goroutine 数持续增长,说明即使功能断言通过,组件生命周期仍错误。
学完本讲,你应该能把伪代码中的一次原子动作拆成锁内状态迁移、锁外阻塞工作和回来后的版本验证;能解释 Mutex/channel/Cond/WaitGroup 各自表达的关系;能从不变量而非日志数量组织调试。随后进入 Raft 时,语言将退到背景,协议证据会成为主线。
教案覆盖地图
覆盖口径:教师 notes/讲义原文逐行完整保留;中文教学单元覆盖课堂机制、失败路径与工程取舍;1 幅辅助机制图;论文另设“问题—机制—证据—边界”阅读导航。覆盖不是用摘要替代原文,任何细节都可在页面末尾回查。
1,166 行 · 5,541 词 · 完整可搜索文本
144 行 · 908 词 · 完整可搜索文本
展开中文教学单元映射(11 项)
- 01Go 语法不是目标:目标是把协议的不变量安全地放进并发程序
- 02并发不是并行的同义词
- 03pipeline 与 fan-out/fan-in
- 04context 与结构化生命周期
- 05接口、错误与测试
- 06用类型、接口与所有权缩小并发表面积
- 07在写代码前完成一张状态所有权表
- 08happens-before:同步原语真正保证了什么
- 09解锁发 RPC 后,怎样证明迟到结果不会污染现在
- 10把并发错误变成可重放的证据
- 11把并发 bug 变成可复现证据,而不是靠多打印几行
论文要读到哪里
Go 的语言与同步原语如何影响分布式协议正确性?
通过明确状态所有权、锁域、happens-before、channel 生命周期和版本校验来控制并发。
把每个示例转换成状态表:谁写、谁读、由什么同步、何时退出;再用 race detector 验证实现而非证明协议。
无 data race 不代表没有逻辑竞态;分别 atomic 的字段也不能自动维护跨字段不变量。
把直觉校准成不变量
关闭 channel 是接收者用来通知发送者停止的通用办法。
通常应由发送方关闭;停止信号应使用独立 done/context,避免 send-on-closed-channel。
只记住正常路径就足以实现协议。
分布式协议的正确性主要由超时、重试、重排、崩溃恢复和旧消息路径决定。
知识检查
谁通常应该关闭一个传递结果的 channel?
下列哪项最准确概括本讲的主要工程取舍?
为什么“关闭 channel 是接收者用来通知发送者停止的通用办法。”是错误的?
离开本讲前,你应能复述
- goroutine 是独立执行的函数,运行时把大量 goroutine 多路复用到线程。
- 轻量 goroutine 让并发表达简单,但若没有背压和取消,简单启动会演变成泄漏与资源失控。
- 通常应由发送方关闭;停止信号应使用独立 done/context,避免 send-on-closed-channel。
完整官方资料附录
以下是本讲对应官方材料的可搜索离线文本。中文精读负责解释;资料附录保留原始细节、例子、问答与代码,不以摘要替代原文。
PDF 文本转录notes/Go-MIT6824-2026.pdf1,166 行 · 5,541 词 · 完整收录
Patterns and Hints
for Concurrency in Go
Russ Cox
MIT 6.5840 / “Spring” 2026
Concurrency is not Parallelism
Concurrency: composition of independently executing processes.
Parallelism: simultaneous execution of (possibly related)
computations.
Concurrency is about dealing with lots of things at once.
Parallelism is about doing lots of things at once.
Prologue:
Goroutines for State
/"([^"\\]|\\.)*"/
state := 0
for {
c := read()
switch state {
case 0:
if c != '"' {
return false
}
state = 1
case 1:
if c == '"' {
return true
}
if c == '\\' {
state = 2
} else {
state = 1
}
case 2:
state = 1
}
}
state := 0
for {
c := read()
switch state {
case 0:
if c != '"' {
return false
}
state = 1
case 1:
if c == '"' {
return true
}
if c == '\\' {
state = 2
} else {
state = 1
}
case 2:
state = 1
}
}
state := 0
for {
switch state {
case 0:
c := read()
if c != '"' {
return false
}
state = 1
case 1:
c := read()
if c == '"' {
return true
}
if c == '\\' {
state = 2
} else {
state = 1
}
case 2:
read()
state = 1
}
}
state := 0
for {
switch state {
case 0:
c := read()
if c != '"' {
return false
}
state = 1
case 1:
c := read()
if c == '"' {
return true
}
if c == '\\' {
state = 2
} else {
state = 1
}
case 2:
read()
state = 1
}
}
state0:
c := read()
if c != '"' {
return false
}
goto state1
state1:
c := read()
if c == '"' {
return true
}
if c == '\\' {
goto state2
} else {
goto state1
}
state2:
read()
goto state1
state0:
c := read()
if c != '"' {
return false
}
goto state1
state1:
c := read()
if c == '"' {
return true
}
if c == '\\' {
goto state2
} else {
goto state1
}
state2:
read()
goto state1
state0:
c := read()
if c != '"' {
return false
}
state1:
c := read()
if c == '"' {
return true
}
if c == '\\' {
goto state2
} else {
goto state1
}
state2:
read()
goto state1
state0: c := read() if c != '"' { return false } state1: c := read() if c == '"' { return true } if c == '\\' { goto state2 } else { goto state1 } state2: read() goto state1
state0: c := read() if c != '"' { return false } state1: c := read() if c == '"' { return true } if c == '\\' { read() goto state1 } else { goto state1 }
state0: c := read() if c != '"' { return false } state1: c := read() if c == '"' { return true } if c == '\\' { read() } goto state1
state0: c := read() if c != '"' { return false } state1: c := read() if c == '"' { return true } if c == '\\' { read() goto state1 } else { goto state1 }
state0: c := read() if c != '"' { return false } state1: c := read() if c == '"' { return true } if c == '\\' { read() } goto state1
c := read() if c != '"' { return false } for { c := read() if c == '"' { return true } if c == '\\' { read() } }
c := read() if c != '"' { return false } for { c := read() if c == '"' { return true } if c == '\\' { read() } }
if read() != '"' { return false } var c rune for c != '"' { c = read() if c == '\\' { read() } } return true
if read() != '"' {
return false
}
inEscape := false
for {
c := read()
if inEscape {
inEscape = false
continue
}
if c == '"' {
return true
}
if c == '\\' {
inEscape = true
}
}
if read() != '"' {
return false
}
var c rune
for c != '"' {
c = read()
if c == '\\' {
read()
}
}
return true
func parse(read func() rune) bool { if read() != '"' { return false } var c rune for c != '"' { c = read() if c == '\\' { read() } } return true }
state==0 →
state==1 →
state==2 →
Hint: Convert data state into code state
when it makes programs clearer.
← inEscape==false
← inEscape==true
type quoter struct { state int } func (q *quoter) Init() { r.state = 0 } func (q *quoter) Write(c rune) Status { switch q.state { case 0: if c != '"' { return BadInput } q.state = 1 case 1: if c == '"' { return Success } if c == '\\' { q.state = 2 } else { q.state = 1 } case 2: q.state = 1 } return NeedMoreInput }
type quoter struct { char chan rune status chan Status } func (q *quoter) Init() { q.char = make(chan rune) q.status = make(chan Status) go q.parse() <-q.status // always NeedMoreInput } func (q *quoter) Write(c rune) Status { q.char <- c return <-q.status } Hint: Use additional goroutines
to hold additional code state.
func (q *quoteReader) parse() { if q.read() != '"' { q.status <- SyntaxError return } var c rune for c != '"' { c = q.read() if c == '\\' { q.read() } } q.status <- Done } func (q *quoter) read() int { q.status <- NeedMoreInput return <-q.char }
package main import ( "net/http" _ "net/http/pprof" ) var c = make(chan int) func main() { for i := range 100 { go f(0x10*i) } http.ListenAndServe("localhost:8080", nil) } func f(x int) { g(x+1) } func g(x int) { h(x+1) } func h(x int) { c <- 1 f(x+1) }
Hint: Know why and when
each goroutine will exit.
$ go run x.go
^\
SIGQUIT: quit
PC=0x105a17b m=0 sigcode=0
...
goroutine 18 [chan send]:
main.h(0x12)
/tmp/x.go:26 +0x45
main.g(0x11)
/tmp/x.go:22 +0x20
main.f(0x10)
/tmp/x.go:18 +0x20
created by main.main
/tmp/x.go:12 +0x42
goroutine 19 [chan send]:
main.h(0x22)
/tmp/x.go:26 +0x45
main.g(0x21)
/tmp/x.go:22 +0x20
main.f(0x20)
/tmp/x.go:18 +0x20
created by main.main
/tmp/x.go:12 +0x42
...
Hint: Type Ctrl-\ to kill a program and
dump all its goroutine stacks.
goroutine profile: total 106
100 @ 0x12d8715 0x12d86c0 0x12d8690 0x1058d61
# 0x12d8714 main.h+0x44 /tmp/x.go:26
# 0x12d86bf main.g+0x1f /tmp/x.go:22
# 0x12d868f main.f+0x1f /tmp/x.go:18
2 @ 0x11ddfcf 0x11dddcf 0x1248265 0x124f513 0x1253636 0x1058d61
# 0x11ddfce net/textproto.(*Reader).readLineSlice+0x5e go/src/net/textproto/reader.go:55
# 0x11dddce net/textproto.(*Reader).ReadLine+0x2e go/src/net/textproto/reader.go:36
# 0x1248264 net/http.readRequest+0xa4 go/src/net/http/request.go:926
# 0x124f512 net/http.(*conn).readRequest+0x1b2 go/src/net/http/server.go:934
# 0x1253635 net/http.(*conn).serve+0x495 go/src/net/http/server.go:1763
1 @ 0x115a102 0x116b1cd 0x124dc92 0x1058d61
# 0x115a101 net.(*netFD).Read+0x51 go/src/net/fd_unix.go:207
# 0x116b1cc net.(*conn).Read+0x6c go/src/net/net.go:182
# 0x124dc91 net/http.(*connReader).backgroundRead+0x61 go/src/net/http/server.go:656
1 @ 0x12cfe22 0x12cfc20 0x12cc6e5 0x12d8051 0x12d8365 0x1254b84 0x1255fa0 0x1257312 0x1253845
0x1058d61
# 0x12cfe21 runtime/pprof.writeRuntimeProfile+0xa1 go/src/runtime/pprof/pprof.go:634
# 0x12cfc1f runtime/pprof.writeGoroutine+0x9f go/src/runtime/pprof/pprof.go:596
# 0x12cc6e4 runtime/pprof.(*Profile).WriteTo+0x3b4 go/src/runtime/pprof/pprof.go:310
# 0x12d8050 net/http/pprof.handler.ServeHTTP+0x1d0 go/src/net/http/pprof/pprof.go:232
# 0x12d8364 net/http/pprof.Index+0x1e4 go/src/net/http/pprof/pprof.go:244
# 0x1254b83 net/http.HandlerFunc.ServeHTTP+0x43 go/src/net/http/server.go:1942
# 0x1255f9f net/http.(*ServeMux).ServeHTTP+0x12f go/src/net/http/server.go:2242
# 0x1257311 net/http.serverHandler.ServeHTTP+0x91 go/src/net/http/server.go:2572
# 0x1253844 net/http.(*conn).serve+0x6a4 go/src/net/http/server.go:1825
Hint: Use the HTTP server’s
/debug/pprof/goroutine
to inspect live goroutine stacks.
Pattern #1
Publish/subscribe server
type PubSub interface {
// Publish publishes the event e to
// all current subscriptions.
Publish(e Event)
// Subscribe registers c to receive future events.
// All subscribers receive events in the same order,
// and that order respects program order:
// if Publish(e1) happens before Publish(e2),
// subscribers receive e1 before e2.
Subscribe(c chan<- Event)
// Cancel cancels the prior subscription of channel c.
// After any pending already-published events
// have been sent on c, the server will signal that the
// subscription is cancelled by closing c.
Cancel(c chan<- Event)
}
type PubSub interface {
// Publish publishes the event e to
// all current subscriptions.
Publish(e Event)
// Subscribe registers c to receive future events.
// All subscribers receive events in the same order,
// and that order respects program order:
// if Publish(e1) happens before Publish(e2),
// subscribers receive e1 before e2.
Subscribe(c chan<- Event)
// Cancel cancels the prior subscription of channel c.
// After any pending already-published events
// have been sent on c, the server will signal that the
// subscription is cancelled by closing c.
Cancel(c chan<- Event)
}
Hint: Close a channel to signal
that no more values will be sent.
type Server struct { mu sync.Mutex sub map[chan<- Event]bool } func (s *Server) Init() { s.sub = make(map[chan<- Event]bool) } func (s *Server) Publish(e Event) { s.mu.Lock() defer s.mu.Unlock() for c := range s.sub { c <- e } } func (s *Server) Subscribe(c chan<- Event) { s.mu.Lock() defer s.mu.Unlock() if s.sub[c] { panic("pubsub: already subscribed") } s.sub[c] = true } func (s *Server) Cancel(c chan<- Event) { s.mu.Lock() defer s.mu.Unlock() if !s.sub[c] { panic("pubsub: not subscribed") } close(c) delete(s.sub, c) }
type Server struct { mu sync.Mutex sub map[chan<- Event]bool } func (s *Server) Init() { s.sub = make(map[chan<- Event]bool) } func (s *Server) Publish(e Event) { s.mu.Lock() defer s.mu.Unlock() for c := range s.sub { c <- e } } func (s *Server) Subscribe(c chan<- Event) { s.mu.Lock() defer s.mu.Unlock() if s.sub[c] { panic("pubsub: already subscribed") } s.sub[c] = true } func (s *Server) Cancel(c chan<- Event) { s.mu.Lock() defer s.mu.Unlock() if !s.sub[c] { panic("pubsub: not subscribed") } close(c) delete(s.sub, c) } Hint: Prefer defer for unlocking mutexes.
type Server struct { mu sync.Mutex sub map[chan<- Event]bool } func (s *Server) Init() { s.sub = make(map[chan<- Event]bool) } func (s *Server) Publish(e Event) { s.mu.Lock() defer s.mu.Unlock() for c := range s.sub { c <- e } } func (s *Server) Subscribe(c chan<- Event) { s.mu.Lock() defer s.mu.Unlock() if s.sub[c] { panic("pubsub: already subscribed") } s.sub[c] = true } func (s *Server) Cancel(c chan<- Event) { s.mu.Lock() defer s.mu.Unlock() if !s.sub[c] { panic("pubsub: not subscribed") } close(c) delete(s.sub, c) }
Hint: Consider the effect
of slow goroutines.
Options for slow goroutines
•Slow down event generation.
•Drop events.
Examples: os/signal, runtime/pprof
•Queue an arbitrary number of events.
Hint: Think carefully before
introducing unbounded queuing.
type Server struct { mu sync.Mutex sub map[chan<- Event]bool } func (s *Server) Init() { s.sub = make(map[chan<- Event]bool) }
type Server struct { publish chan Event subscribe chan subReq cancel chan subReq } type subReq struct { c chan<- Event ok chan bool } func (s *Server) Init() { s.publish = make(chan Event) s.subscribe = make(chan subReq) s.cancel = make(chan subReq) go s.loop() }
func (s *Server) Publish(e Event) { s.mu.Lock() defer s.mu.Unlock() for c := range s.sub { c <- e } } func (s *Server) Subscribe(c chan<- Event) { s.mu.Lock() defer s.mu.Unlock() if s.sub[c] { panic("pubsub: already subscribed") } s.sub[c] = true } func (s *Server) Cancel(c chan<- Event) { s.mu.Lock() defer s.mu.Unlock() if !s.sub[c] { panic("pubsub: not subscribed") } close(c) delete(s.sub, c) }
func (s *Server) loop() {
sub := make(map[chan<- Event]bool)
for {
select {
case e := <-s.publish:
for c := range sub {
c <- e
}
case r := <-s.subscribe:
if sub[r.c] {
r.ok <- false
break
}
sub[r.c] = true
r.ok <- true
case c := <-s.cancel:
if !sub[r.c] {
r.ok <- false
break
}
close(r.c)
delete(sub, r.c)
r.ok <- true
}
}
}
func (s *Server) Publish(e Event) {
s.mu.Lock()
defer s.mu.Unlock()
for c := range s.sub {
c <- e
}
}
func (s *Server) Subscribe(c chan<- Event) {
s.mu.Lock()
defer s.mu.Unlock()
if s.sub[c] {
panic("pubsub: already subscribed")
}
s.sub[c] = true
}
func (s *Server) Cancel(c chan<- Event) {
s.mu.Lock()
defer s.mu.Unlock()
if !s.sub[c] {
panic("pubsub: not subscribed")
}
close(c)
delete(s.sub, c)
}
func (s *Server) Publish(e Event) {
s.publish <- e
}
func (s *Server) Subscribe(c chan<- Event) {
r := subReq{c: c, ok: make(chan bool)}
s.subscribe <- r
if !<-r.ok {
panic("pubsub: already subscribed")
}
}
func (s *Server) Cancel(c chan<- Event) {
r := subReq{c: c, ok: make(chan bool)}
s.cancel <- r
if !<-r.ok {
panic("pubsub: not subscribed")
}
}
type Server struct {
publish chan Event
subscribe chan subReq
cancel chan subReq
}
type subReq struct {
c chan<- Event
ok chan bool
}
func (s *Server) Init() {
s.publish = make(chan Event)
s.subscribe = make(chan subReq)
s.cancel = make(chan subReq)
go s.loop()
}
func (s *Server) Publish(e Event) {
s.publish <- e
}
func (s *Server) Subscribe(c chan<- Event) {
r := subReq{c: c, ok: make(chan bool)}
s.subscribe <- r
if !<-r.ok {
panic("pubsub: already subscribed")
}
}
func (s *Server) Cancel(c chan<- Event) {
r := subReq{c: c, ok: make(chan bool)}
s.cancel <- r
if !<-r.ok {
panic("pubsub: not subscribed")
}
}
func (s *Server) loop() {
sub := make(map[chan<- Event]bool)
for {
select {
case e := <-s.publish:
for c := range sub {
c <- e
}
case r := <-s.subscribe:
if sub[r.c] {
r.ok <- false
break
}
sub[r.c] = true
r.ok <- true
case c := <-s.cancel:
if !sub[r.c] {
r.ok <- false
break
}
close(r.c)
delete(sub, r.c)
r.ok <- true
}
}
}
Hint: Convert mutexes
into goroutines
when it makes programs clearer
func helper(in <-chan Event,
out chan<- Event) {
var q []Event
for {
select {
case e := <-in:
q = append(q, e)
case out <- q[0]:
q = q[1:]
}
}
}
func helper(in <-chan Event,
out chan<- Event) {
var q []Event
for {
select {
case e := <-in:
q = append(q, e)
case out <- q[0]:
q = q[1:]
}
}
}
func helper(in <-chan Event,
out chan<- Event) {
var q []Event
for {
// Decide whether and what to send.
var sendOut chan<- Event
var next Event
if len(q) > 0 {
sendOut = out
next = q[0]
}
select {
case e := <-in:
q = append(q, e)
case sendOut <- next:
q = q[1:]
}
}
}
func helper(in <-chan Event,
out chan<- Event) {
var q []Event
for {
// Decide whether and what to send.
var sendOut chan<- Event
var next Event
if len(q) > 0 {
sendOut = out
next = q[0]
}
select {
case e := <-in:
q = append(q, e)
case sendOut <- next:
q = q[1:]
}
}
}
func helper(in <-chan Event,
out chan<- Event) {
var q []Event
for in != nil || len(q) > 0 {
// Decide whether and what to send.
var sendOut chan<- Event
var next Event
if len(q) > 0 {
sendOut = out
next = q[0]
}
select {
case e, ok := <-in:
if !ok {
in = nil // stop receiving from in
break
}
q = append(q, e)
case sendOut <- next:
q = q[1:]
}
}
close(out)
}
func (s *Server) loop() {
sub := make(map[chan<- Event]bool)
for {
select {
case e := <-s.publish:
for c := range sub {
c <- e
}
case r := <-s.subscribe:
if sub[r.c] {
r.ok <- false
break
}
sub[r.c] = true
r.ok <- true
case c := <-s.cancel:
if !sub[r.c] {
r.ok <- false
break
}
close(r.c)
delete(sub, r.c)
r.ok <- true
}
}
}
func (s *Server) loop() {
sub := make(map[chan<- Event]bool)
for {
select {
case e := <-s.publish:
for c := range sub {
c <- e
}
case r := <-s.subscribe:
if sub[r.c] {
r.ok <- false
break
}
sub[r.c] = true
r.ok <- true
case c := <-s.cancel:
if !sub[r.c] {
r.ok <- false
break
}
close(r.c)
delete(sub, r.c)
r.ok <- true
}
}
}
func (s *Server) loop() {
sub := make(map[chan<- Event]chan<- Event)
for {
select {
case e := <-s.publish:
for _, h := range sub {
h <- e
}
case r := <-s.subscribe:
if sub[r.c] != nil {
r.ok <- false
break
}
h = make(chan Event)
go helper(h, r.c)
sub[r.c] = h
r.ok <- true
case c := <-s.cancel:
if sub[r.c] == nil {
r.ok <- false
break
}
close(sub[r.c])
delete(sub, r.c)
r.ok <- true
}
}
}
Hint: Use goroutines
to let independent concerns
run independently.
Pattern #2
Work scheduler
func Schedule(servers []string, numTask int,
call func(srv string, task int))
func Schedule(servers []string, numTask int, call func(srv string, task int)) { idle := make(chan string, len(servers)) for _, srv := range servers { idle <- srv } }
Hint: Use a buffered channel
as a concurrent blocking queue.
func Schedule(servers []string, numTask int, call func(srv string, task int)) { idle := make(chan string, len(servers)) for _, srv := range servers { idle <- srv } for task := range numTask { go func() { srv := <-idle call(srv, task) idle <- srv }() } }
Hint: Use goroutines
to let independent concerns
run independently.
func Schedule(servers []string, numTask int, call func(srv string, task int)) { idle := make(chan string, len(servers)) for _, srv := range servers { idle <- srv } for task := range numTask { go func() { srv := <-idle call(srv, task) idle <- srv }() } }
Hint: Think carefully before
introducing unbounded queuing.
func Schedule(servers []string, numTask int, call func(srv string, task int)) { idle := make(chan string, len(servers)) for _, srv := range servers { idle <- srv } for task := range numTask { go func() { srv := <-idle call(srv, task) idle <- srv }() } }
for task := range numTask { srv := <-idle go func() { call(srv, task) idle <- srv }() }
func Schedule(servers []string, numTask int, call func(srv string, task int)) { idle := make(chan string, len(servers)) for _, srv := range servers { idle <- srv } for task := range numTask { srv := <-idle go func() { call(srv, task) idle <- srv }() } for range servers { <-idle } }
func Schedule(servers []string, numTask int, call func(srv string, task int)) { idle := make(chan string, len(servers)) for _, srv := range servers { idle <- srv } for task := range numTask { srv := <-idle go func() { call(srv, task) idle <- srv }() } for range servers { <-idle } }
func Schedule(servers []string, numTask int, call func(srv string, task int)) { work := make(chan int) done := 0 runTasks := func(srv string) { for task := range work { call(srv, task) } done++ } for _, srv := range servers { go runTasks(srv) } for task := range numTask { work <- task } close(work) for done < len(servers) { runtime.Gosched() } }
Hint: Think carefully before
introducing unbounded queuing.
Hint: Close a channel to signal
that no more values will be sent.
func Schedule(servers []string, numTask int, call func(srv string, task int)) { work := make(chan int) done := 0 runTasks := func(srv string) { for task := range work { call(srv, task) } done++ } for _, srv := range servers { go runTasks(srv) } for task := range numTask { work <- task } close(work) for done < len(servers) { runtime.Gosched() } }
$ go run -race /tmp/x.go
==================
WARNING: DATA RACE
Write at 0x00c0000121d8 by goroutine 6:
main.Schedule.func1()
/tmp/x.go:19 +0x80
main.Schedule.gowrap1()
/tmp/x.go:23 +0x48
Previous read at 0x00c0000121d8 by main goroutine:
main.Schedule()
/tmp/x.go:31 +0x27c
main.main()
/tmp/x.go:6 +0x90
Goroutine 6 (running) created at:
main.Schedule()
/tmp/x.go:23 +0x13c
main.main()
/tmp/x.go:6 +0x90
==================
Hint: Use the race detector,
for development and even production.
func Schedule(servers []string, numTask int, call func(srv string, task int)) { work := make(chan int) done := 0 runTasks := func(srv string) { for task := range work { call(srv, task) } done++ } for _, srv := range servers { go runTasks(srv) } for task := range numTask { work <- task } close(work) for done < len(servers) { runtime.Gosched() } }
$ go run -race /tmp/x.go ================== WARNING: DATA RACE Write at 0x00c00019c008 by goroutine 7: main.Schedule.func1() /tmp/x.go:19 +0x80 main.Schedule.gowrap1() /tmp/x.go:23 +0x48 Previous write at 0x00c00019c008 by goroutine 6: main.Schedule.func1() /tmp/x.go:19 +0x80 main.Schedule.gowrap1() /tmp/x.go:23 +0x48| Goroutine 7 (running) created at: main.Schedule() /tmp/x.go:23 +0x13c main.main() /tmp/x.go:6 +0x90 Goroutine 6 (finished) created at: main.Schedule() /tmp/x.go:23 +0x13c main.main() /tmp/x.go:6 +0x90 ==================
func Schedule(servers []string, numTask int, call func(srv string, task int)) { work := make(chan int) done := 0 runTasks := func(srv string) { for task := range work { call(srv, task) } done++ } for _, srv := range servers { go runTasks(srv) } for task := range numTask { work <- task } close(work) for done < len(servers) { runtime.Gosched() } }
Hint:
Don’t communicate by sharing memory.
Share memory by communicating.
work := make(chan int) done := make(chan bool) runTasks := func(srv string) { for task := range work { call(srv, task) } done <- true } ... for range servers { <-done }
func Schedule(servers []string, numTask int, call func(srv string, task int)) { work := make(chan int) done := make(chan bool) runTasks := func(srv string) { for task := range work { call(srv, task) } done <- true } for _, srv := range servers { go runTasks(srv) } for task := range numTask { work <- task } close(work) for range servers { <-done } }
func Schedule(servers chan string, numTask int, call func(srv string, task int)) { go func() { for srv := range servers { go runTasks(srv) } }()
Hint: Use goroutines
to let independent concerns
run independently.
func Schedule(servers chan string, numTask int, call func(srv string, task int)) { work := make(chan int) done := make(chan bool) runTasks := func(srv string) { for task := range work { call(srv, task) } done <- true } go func() { for _, srv := range servers { go runTasks(srv) } }() for range numTask { work <- task } close(work) for range servers { <-done } }
runTasks := func(srv string) { for task := range work { call(srv, task) done <- true } } for range numTask { <-done }
func Schedule(servers chan string, numTask int, call func(srv string, task int)) { work := make(chan int) done := make(chan bool) runTasks := func(srv string) { for task := range work { call(srv, task) done <- true } } go func() { for _, srv := range servers { go runTasks(srv) } }() for range numTask { work <- task } close(work) for range numTask { <-done } }
Hint: Know why and when
each communication will proceed.
$ go run /tmp/x.go
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
main.Schedule(0xc4200120c0, 0x3, 0x3, 0x14, 0x106acc8)
/tmp/x.go:26 +0x150
main.main()
/tmp/x.go:4 +0x96
goroutine 5 [chan send]:
main.Schedule.func1(0x1066bc0, 0x1)
/tmp/x.go:15 +0xba
created by main.Schedule.func2
/tmp/x.go:21 +0x5f
func Schedule(servers chan string, numTask int, call func(srv string, task int)) { work := make(chan int) done := make(chan bool) runTasks := func(srv string) { for task := range work { call(srv, task) done <- true } } go func() { for _, srv := range servers { go runTasks(srv) } }() for task := range numTask { work <- task } close(work) for i := range numTask { <-done } }
i := 0 WorkLoop: for task := range numTask { for { select { case work <- task: continue WorkLoop case <-done: i++ } } } close(work) for ; i < numTask; i++ { <-done }
func Schedule(servers chan string, numTask int, call func(srv string, task int)) { work := make(chan int) done := make(chan bool) runTasks := func(srv string) { for task := range work { call(srv, task) done <- true } } go func() { for _, srv := range servers { go runTasks(srv) } }() for task := range numTask { work <- task } close(work) for range numTask { <-done } }
go func() { for task := range numTask { work <- task } close(work) }()
Hint: Use goroutines
to let independent concerns
run independently.
func Schedule(servers chan string, numTask int, call func(srv string, task int)) { work := make(chan int) done := make(chan bool) runTasks := func(srv string) { for task := range work { call(srv, task) done <- true } } go func() { for _, srv := range servers { go runTasks(srv) } }() for task := range numTask { work <- task } close(work) for range numTask { <-done } }
work := make(chan int, numTask)
Hint: Think carefully before
introducing unbounded queuing.
func Schedule(servers chan string, numTask int, call func(srv string, task int)) { work := make(chan int, numTask) done := make(chan bool) runTasks := func(srv string) { for task := range work { call(srv, task) done <- true } } go func() { for _, srv := range servers { go runTasks(srv) } }() for task := range numTask { work <- task } close(work) for range numTask { <-done } }
func Schedule(servers chan string, numTask int, call func(srv string, task int) bool) { work := make(chan int, numTask) done := make(chan bool) runTasks := func(srv string) { for task := range work { call(srv, task) done <- true } } go func() { for _, srv := range servers { go runTasks(srv) } }() for task := range numTask { work <- task } close(work) for range numTask { <-done } }
runTasks := func(srv string) { for task := range work { if call(srv, task) { done <- true } else { work <- task } } } ... for task := range numTask { work <- task } for range numTask { <-done } close(work)
Hint: Know why and when
each communication will proceed.
func Schedule(servers chan string, numTask int, call func(srv string, task int) bool) { work := make(chan int, numTask) done := make(chan bool) runTasks := func(srv string) { for task := range work { call(srv, task) done <- true } } go func() { for _, srv := range servers { go runTasks(srv) } }() for task := range numTask { work <- task } close(work) for range numTask { <-done } }
runTasks := func(srv string) { for task := range work { if call(srv, task) { done <- true } else { work <- task } } } ... for task := range numTask { work <- task } for range numTask { <-done } close(work) Hint: Close a channel to signal
that no more values will be sent.
func Schedule(servers chan string, numTask int, call func(srv string, task int) bool) { work := make(chan int, numTask) done := make(chan bool) runTasks := func(srv string) { for task := range work { if call(srv, task) { done <- true } else { work <- task } } } go func() { for _, srv := range servers { go runTasks(srv) } }() for task := range numTask { work <- task } for range numTask { <-done } close(work) }
func Schedule(servers chan string, numTask int, call func(srv string, task int) bool) { work := make(chan int, numTask) done := make(chan bool) runTasks := func(srv string) { for task := range work { if call(srv, task) { done <- true } else { work <- task } } } go func() { for _, srv := range servers { go runTasks(srv) } }() for task := range numTask { work <- task } for range numTask { <-done } close(work) }
work := make(chan int, numTask) done := make(chan bool) exit := make(chan bool) ... go func() { for { select { case srv := <-servers: go runTasks(srv) case <-exit: return } } }() ... for range numTask { <-done } close(work) exit <- true
Hint: Make sure you know
why and when each goroutine will exit.
Pattern #3
Replicated service client
type ReplicatedClient interface {
// Init initializes the client to use the given servers.
// To make a particular request later,
// the client can use callOne(srv, args), where srv
// is one of the servers from the list.
Init(servers []string, callOne func(string, Args) Reply)
// Call makes a request on any available server.
// Multiple goroutines may call Call concurrently.
Call(args Args) Reply
}
type Client struct { servers []string callOne func(string, Args) Reply mu sync.Mutex prefer int } func (c *Client) Init(servers []string, callOne func(string, Args) Reply) { c.servers = servers c.callOne = callOne }
Hint: Use a mutex if that is
the clearest way to write the code.
type Client struct {
servers []string
callOne func(string, Args) Reply
mu sync.Mutex
prefer int
}
func (c *Client) Init(servers []string, callOne func(string, Args) Reply) {
c.servers = servers
c.callOne = callOne
}
func (c *Client) Call(args Args) Reply {
type result struct {
serverID int
reply Reply
}
done := make(chan result, 1)
id := ...
go func() {
done <- result{id, c.callOne(c.servers[id], args)}
}()
}
Hint: Use goroutines
to let independent concerns
run independently.
func (c *Client) Call(args Args) Reply { type result struct { serverID int reply Reply } const timeout = 1 * time.Second t := time.NewTimer(timeout) defer t.Stop() done := make(chan result, 1) id := ... go func() { done <- result{id, c.callOne(c.servers[id], args)} }() select { case r := <-done: return r.reply case <-t.C: // timeout } }
Hint: Stop timers you don’t need.
(fixed in Go 1.23)
Hint: Know why and when
each goroutine will exit.
Hint: Know why and when
each communication will proceed.
func (c *Client) Call(args Args) Reply { type result struct { serverID int reply Reply } const timeout = 1 * time.Second t := time.NewTimer(timeout) defer t.Stop() done := make(chan result, len(c.servers)) for id := range c.servers { go func() { done <- result{id, c.callOne(c.servers[id], args)} }() select { case r := <-done: return r.reply case <-t.C: // timeout t.Reset(timeout) } } r := <-done return r.reply }
c.mu.Lock() prefer := c.prefer c.mu.Unlock() var r result for off := range c.servers { id := (prefer + off) % len(c.servers) go func() { done <- result{id, c.callOne(c.servers[id], args)} }() select { case r = <-done: goto Done case <-t.C: // timeout t.Reset(timeout) } } r = <-done Done: c.mu.Lock() c.prefer = r.serverID c.mu.Unlock() return r.reply
Hint: Use a goto if that is
the clearest way to write the code.
Pattern #4
Protocol multiplexer
type ProtocolMux interface {
// Init initializes the mux to manage messages to the given service.
Init(Service)
// Call makes a request with the given message and returns the reply.
// Multiple goroutines may call Call concurrently.
Call(Msg) Msg
}
type Service interface {
// ReadTag returns the muxing identifier in the request or reply message.
// Multiple goroutines may call ReadTag concurrently.
ReadTag(Msg) int64
// Send sends a request message to the remote service.
// Send must not be called concurrently with itself.
Send(Msg)
// Recv waits for and returns a reply message from the remote service.
// Recv must not be called concurrently with itself.
Recv() Msg
}
type Mux struct {
srv Service
send chan Msg
mu sync.Mutex
pending map[int64]chan<- Msg
}
func (m *Mux) Init(srv Service) {
m.srv = srv
m.pending = make(map[int64]chan Msg)
go m.sendLoop()
go m.recvLoop()
}
type Mux struct {
srv Service
send chan Msg
mu sync.Mutex
pending map[int64]chan<- Msg
}
func (m *Mux) Init(srv Service) {
m.srv = srv
m.pending = make(map[int64]chan Msg)
go m.sendLoop()
go m.recvLoop()
}
func (m *Mux) sendLoop() {
for args := range m.send {
m.srv.Send(args)
}
}
func (m *Mux) sendLoop() { for args := range m.send { m.srv.Send(args) } } func (m *Mux) recvLoop() { for { reply := m.srv.Recv() tag := m.srv.ReadTag(reply) m.mu.Lock() done := m.pending[tag] delete(m.pending, tag) m.mu.Unlock() if done == nil { panic("unexpected reply") } done <- reply } }
func (m *Mux) sendLoop() { for args := range m.send { m.srv.Send(args) } } func (m *Mux) recvLoop() { for { reply := m.srv.Recv() tag := m.srv.Tag(reply) m.mu.Lock() done := m.pending[tag] delete(m.pending, tag) m.mu.Unlock() if done == nil { panic("unexpected reply") } done <- reply } }
func (m *Mux) Call(args Msg) (reply Msg) { tag := m.srv.ReadTag(args) done := make(chan Msg, 1) m.mu.Lock() if m.pending[tag] != nil { m.mu.Unlock() panic("mux: duplicate call tag") } m.pending[tag] = done m.mu.Unlock() m.send <- args return <-done }
Hint: Use goroutines, channels,
and mutexes together if that is
the clearest way to write the code.
Hints
Use the race detector, for development and even production.
Don’t communicate by sharing memory. Share memory by communicating.
Convert data state into code state when it makes programs clearer.
Convert mutexes into goroutines when it makes programs clearer.
Use additional goroutines to hold additional code state.
Use goroutines to let independent concerns run independently.
Consider the effect of slow goroutines.
Know why and when each communication will proceed.
Know why and when each goroutine will exit.
Type Ctrl-\ to kill a program and dump all its goroutine stacks.
Use the HTTP server’s /debug/pprof/goroutine to inspect live goroutine stacks.
Use a buffered channel as a concurrent blocking queue.
Think carefully before introducing unbounded queuing.
Close a channel to signal that no more values will be sent.
Use a mutex if that is the clearest way to write the code.
Prefer defer for unlocking mutexes.
Use a goto if that is the clearest way to write the code.
Use goroutines, channels, and mutexes together
if that is the clearest way to write the code.论文 FAQpapers/go-faq.txt144 行 · 908 词 · 完整收录
Q: Can I stop these complaints about my unused variable/import?
A: There's a good explanation at https://golang.org/doc/faq#unused_variables_and_imports.
Q: Is the defer keyword in other languages?
A: Defer was new in Go. We originally added it to provide a way to
recover from panics (see "recover" in the spec), but it turned out
to be very useful for idioms like "defer mu.Unlock()" as well.
Later, Swift added a defer statement too. It seems clearly inspired
by Go but I'm not sure how close the details are.
Q: Why is the type after the variable declaration, unlike C languages?
A: There's a good explanation at https://blog.golang.org/gos-declaration-syntax.
Q: Why not adopt classes and OOP like in C++ and Java?
A: We believe that Go's approach to object-oriented programming,
which is closer to Smalltalk than to Java/C++/Simula, is more
lightweight and makes it easier to adapt large programs. I talked
about this at Google I/O in 2010. See
https://github.com/golang/go/wiki/GoTalks#go-programming for links
to the video and slides.
Q: Why does struct require a trailing comma on a multiline definition?
A: Originally it didn't, but all statements were terminated by
semicolons. We made semicolons optional shortly after the public
release of Go. When we did that, we tried to avoid Javascript's
mistake of making the semicolon rules very complex and error-prone.
Instead we have a simple rule: every line ends in an implicit
semicolon unless the final token is something that cannot possibly
end a statement (for example, a plus sign, or a comma). One effect
of this is that if you don't put the trailing comma on the line,
it gets an implicit semicolon, which doesn't parse well. It's
unfortunate, and it wasn't that way before the semicolon rules, but
we're so happy about not typing semicolons all the time that we'll
live with it. The original proposal for semicolon insertion is at
https://groups.google.com/d/msg/golang-nuts/XuMrWI0Q8uk/kXcBb4W3rH8J.
See the next answer also.
Q: Why are list definitions inconsistent, where some need commas and some do not?
A: The ones that don't need commas need semicolons, but those
semicolons are being inserted automatically (see previous answer).
The rule is that statements are separated by semicolons and smaller
pieces of syntax by commas:
import "x";
import "y";
var x = []int{
1,
2,
3,
}
When you factor out a group of imports, you still have semicolons:
import (
"x";
"y";
)
var x = []int{
1,
2,
3,
}
But then when we made semicolons optional, the semicolons disappeared
from the statement blocks leaving the commas behind:
import (
"x"
"y"
)
var x = []int{
1,
2,
3,
}
Now the distinction is between nothing and something, instead of
two different characters, and it's more pronounced. If we had known
from the start that semicolons would be optional I think we might
have used them in more syntactic forms, or maybe made some forms
accept either commas or semicolons. At this point that seems
unlikely, though.
Q: Why does Go name its while loops "for"?
A: C has both while(cond) {} and for(;cond;) {}. It didn't seem
like Go needed two keywords for the same thing.
Q: There seem to be a lot of new languages emerging these days,
including Rust, D, Swift and Nim, among probably others. Are there
any lessons you've learned from these other languages and their
communities that you wish you'd been able to incorporate into Go?
A: I do watch those languages for developments. I think they've
learned things from Go and I hope we've also learned things from
them. Some day I'd like the Go compiler to do a better job of
inferring ownership rules, or maybe even having lightweight ownership
expressions in the type system. Javari, Midori, Pony, and Rust are
inspirations here. I wrote a bit more about this at
https://research.swtch.com/go2017.
Q: Why the focus on concurrency and goroutines?
A: We knew from past experience that good concurrency support using
channels and lightweight processes would make writing the kinds of
systems we built at Google a lot easier, as I hope the lecture
showed. There's a bit more explanation at https://golang.org/doc/faq#csp,
and some background about our earlier experiences at
https://swtch.com/~rsc/thread/.
Q: Does Go pass function arguments by value or by reference?
A: For most types (e.g. numbers, structs) Go passes a copy of the
value, so that the caller does not see any modifications that the
callee makes. strings are effectively call-by-value since their
content cannot be modified.
A few built-in types -- channels, maps, and slices -- are effectively
call-by-reference in the sense that updates to the data structure are
seen by both caller and callee.
Calling a method with a pointer receiver passes the object by
reference (by passing a pointer to it), even though the call lacks any
& and thus looks like it should be by value.
This non-uniformity can be confusing. For example, if you want to pass
a sync.WaitGroup to a function, you probably need to pass a pointer to
it with &wg, whereas you should probably pass a channel without any &.
For a complex type like a map or a slice, there's room for
disagreement about what call-by-value and call-by-reference mean. If
you have a variable of type map, and you think the map lives inside
the variable, then Go's maps look like call-by-reference. If you think
the variable contains a pointer to map data in the heap, then Go's
maps look like call-by-value (where the value is the pointer).