在MIT6.824 Go项目中,很多时候大家都知道逻辑上如何实现,但具体到代码中却不知从何做起(尤其对于写惯了Python的同学(‵﹏′))。为了完成该项目,需要掌握的Go相关知识不多,这里简单做一个总结。
GoLand
建议使用GoLand进行代码的编写和调试,有丰富的断点调试、函数提醒,自动补全等等对初学者非常实用的功能。初次使用GoLand,切记在Settings中设置GOROOT和GOPATH环境变量
- 如何设置调试/运行环境?
为开发和测试方便,可为每一个Lab单独设置一个Go Test Configurations。如图,1)直接编译运行3A所有test cases;2)调试运行;3)你的test_test.go脚本所在package;4)在go test运行时,只要函数名包含该pattern关键词都会运行

2. Golang没有传统意义上的try-catch机制,但可通过golang提供了recover机制进行断点调试
func A() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered in f", r) // 断点设于此,当发生error的时,GoLand可保存error发生时的现场
}
} ()
// 正常的函数代码逻辑
}
输出
在该项目中,各种并发、crash等,光靠断点调试是远远不够的,最有力的工具还是print!!
// 输出一个字符串
fmt.Println("error")
// format后输出一个字符串
fmt.Printf("Index: %dn", lastAppliedIndex)
// format后赋值一个字符串
a := fmt.Sprintf("Index: %d", lastAppliedIndex)
// 数字 => 字符串
s := strconv.Itoa(-42)
// 字符串 => 数字
i, err := strconv.Atoi("-42")
if err == nil {
fmt.Println(i)
}
// 在多节点中,给每个goroutine加一个identifier
func uuid4() string {
b := make([]byte, 16)
_, err := craned.Read(b)
if err != nil {
fmt.Println("Error: ", err)
return "no-uuid"
}
return fmt.Sprintf("%X-%X-%X-%X-%X", b[0:4], b[4:6], b[6:8], b[8:10], b[10:])
}
uuid := uuid4()
fmt.Printf("[%s]Index: %dn", uuid, lastAppliedIndex)
Channel
Go通过 channel 进行协程之间的通信来实现数据共享,在Lab1, 2, 3都会用到channel。channel主要有两个操作:1)chan<- data 阻塞写数据;<-chan 阻塞读数据(其也有非阻塞的方案,不过此项目中用不到:Go by Example: Non-Blocking Channel Operations)。下面是一个测试case,有助理解:
func A() {
c1 := make(chan string)
go func() { time.Sleep(5 * time.Second); c1 <- "111" } ()
v1 := <- c1
fmt.Println(v1)
c2 := make(chan string)
go func() { v2 := <- c2; fmt.Println(v2) } ()
time.Sleep(5 * time.Second)
c2 <- "222"
}
利用阻塞读写这一性质,我们可利用一个单独的goroutine作为守护进程来监听来自applyCh的消息。如下,该goroutine是一个死循环,当没有通知的时候,会一直阻塞在那里,而非一直消耗CPU运行。
go func() {
for {
select {
case msg := <-kv.applyCh:
// do something with msg
}
}
} ()
WaitGroup
在该项目中有一种情况:同时执行n多goroutine任务,等待所有goroutine执行结束后,返回结果,这时候可以使用WaitGroup,我们主要涉及三个方法: wg.Add(), wg.Done(), wg.wait()
- wg.Add() 对waitGroup counter对象+1,通常当一个goroutine运行前执行
- wg.Done() 对waitGroup counter对象-1,通常当一个goroutine结束后执行
- wg.Wait() 一直阻塞,直到waitGroup counter对象的值为0
func A() {
var wg sync.WaitGroup
for i:=0; i<10; i++ {
wg.Add(1)
go func B() {
defer func() { wg.Done() } ()
// do some stuff
} ()
}
wg.Wait()
return
}
Timer
在实现Lab2的时候,涉及到Timer的使用,比如心跳包和election timeout的控制。主要实现的功能即:在特定时间后,发送一个通知给Timer chan。以心跳包举例,我们可以每隔1s发送一个消息给heartBeatTimer chan,然后我们使用一个goroutine监控heartBeatTimer chan。在此,主要涉及:timer.Stop(), timer.Reset(), timer.C
- timer.Stop() 暂时停止该timer,比如对于follower来说我们不需要heartBeat
- timer.Reset(duration) 重置时间,时间从头重新计算,这对election尤其重要。
- timer.C 该timer的chan对象
// heartBeat timer
go func() {
for {
select {
case <- rf.heartBeatTimer.C:
rf.heartBeatTimer.Reset(1 * time.Second)
}
}
} ()
Lock
该项目中大量使用了goroutine,因此会面临很多race condition。比如,修改rf.currentTerm的时候。在此项目中,已帮我们封装好Lock,无需过多关心细节,更多需要考虑的是:什么时候,什么地方应该加lock,具体可以参考: http://mpaxos.com/teaching/ds/20fa/labs/raft-locking.txt
rf.mu.Lock()
// modify some shared data
rf.mu.Unlock()