基础部分根据官网的Go指南进行记录,包括基础的语法和并发相关内容。 并包含实际实验中踩到坑。
基础
包,变量,函数
包
每个Go程序都是由包构成的
程序从main包开始运行
按照约定,包名与导入路径的最后一个元素一致。例如,”math/rand” 包中的源码均以 package rand 语句开始
导入
可以使用圆括号组合导入
例如,可以写为
import (
"fmt"
"math"
)
导出名
在Go中,首字母大写是已导出的。在导入一个包时,只能引用其中已导出的名字
例如,
func main() {
fmt.Println(math.pi)
}
会报cannot refer to unexported name math.pi
的错误
函数
与其他语言不同的是参数变量类型在参数变量名后,函数返回类型在参数列表后,函数名前有func
关键字
func add(x int, y int) int {
return x + y
}
连续多个参数类型相同时,可以省略除了最后一个以外的变量类型
func add(x, y int) int {
return x + y
}
函数也可以多值返回,用圆括号包裹多个类型,逗号分隔返回值
func swap(x, y string) (string, string) {
return y, x
}
Go的返回值也可以被命名,声明方式类似于参数列表,使用上类似于matlab中的返回值。没有参数的return会返回已命名的返回值,不过这种直接返回不推荐在长函数中使用,影响代码可读性
func split(sum int) (x, y int) {
x = sum * 4 / 9
y = sum - x
return
}
变量
var语句用于声明变量列表,类型后置
var c, python, java bool
声明的时候可以包含初始值,每个变量一一对应,包含初始值得时候可以省略类型,类型会从初始值中获得
var i, j int = 1, 2
var c, python, java = true, false, "no!"
在函数中,可以使用简洁赋值语句:=
在类型明确的地方代替var
声明,在函数外的每个语句都必须以关键字开始,因此:=
不能再函数外使用
var i, j int = 1, 2
k := 3
c, python, java := true, false, "no!"
没有被明确初始值的变量会被赋予零值,零值包括
- 数值类型为 0 ,
- 布尔类型为 false ,
- 字符串为 “” (空字符串)。
类型
Go的基本类型有:
bool
string
int int8 int16 int32 int64
uint uint8 uint16 uint32 uint64 uintptr
byte // uint8 的别名
rune // int32 的别名
// 表示一个 Unicode 码点
float32 float64
complex64 complex128
基本使用方法:
var (
ToBe bool = false
MaxInt uint64 = 1<<64 - 1
z complex128 = cmplx.Sqrt(-5 + 12i)
)
func main() {
fmt.Printf("Type: %T Value: %v\n", ToBe, ToBe)
fmt.Printf("Type: %T Value: %v\n", MaxInt, MaxInt)
fmt.Printf("Type: %T Value: %v\n", z, z)
}
Type: bool Value: false
Type: uint64 Value: 18446744073709551615
Type: complex128 Value: (2+3i)
Go可以使用表达式T(v)
将值v
转换为类型T
,但是Go不包含隐式类型转换,不同类型之间赋值需要显示转换
var x, y int = 3, 4
var f float64 = math.Sqrt(x*x + y*y)
var z uint = uint(f)
常量的声明与变量类似,把var换为const,不能使用:=
语法声明。常量可以是字符、字符串、布尔值或数值
const World = "世界"
未被指定类型的常量由上下文来决定其类型,下面的代码不会报类型不匹配的错误
const (
// Create a huge number by shifting a 1 bit left 100 places.
// In other words, the binary number that is 1 followed by 100 zeroes.
Big = 1 << 100
// Shift it right again 99 places, so we end up with 1<<1, or 2.
Small = Big >> 99
)
func needInt(x int) int { return x*10 + 1 }
func needFloat(x float64) float64 {
return x * 0.1
}
func main() {
fmt.Println(needInt(Small))
fmt.Println(needFloat(Small))
fmt.Println(needFloat(Big))
}
控制流语句
for
Go只有一种循环结构:for
循环
与其他语言不同的是,Go的for循环不包括小括号,但大括号是必须的
其中初始化语句的”:=”声明的变量只在for作用域中可见
func main() {
sum := 0
for i := 0; i < 10; i++ {
sum += i
}
fmt.Println(sum)
}
与其他语言类似,for循环的初始化语句后和后置语句可以省略
甚至连前后分后都可以同时省略,成为while
for sum < 1000 {
sum += sum
}
循环条件也可以省略,所以在Go中无限循环可以写的很紧凑
for {
}
if
与for语句类似,无需小括号,但必须大括号。与其他语言不同的是,if语句可以像for语句一样,在条件表达式前执行一个简单的语句,同样变量作用域仅在if中可见
func pow(x, n, lim float64) float64 {
if v := math.Pow(x, n); v < lim {
return v
}
return v
}
switch
与其他语言类似的从上到下匹配,不过Go中除非以fallthrough
结尾,否则分支会默认终止。与for,if相似,在变量也可以执行简单语句,分支条件也可以是表达式
func main() {
fmt.Println("When's Saturday?")
switch today := time.Now().Weekday();time.Saturday {
case today + 0:
fmt.Println("Today.")
case today + 1:
fmt.Println("Tomorrow.")
case today + 2:
fmt.Println("In two days.")
default:
fmt.Println("Too far away.")
}
}
省略条件则同switch true一样
func main() {
t := time.Now()
switch {
case t.Hour() < 12:
fmt.Println("Good morning!")
case t.Hour() < 17:
fmt.Println("Good afternoon.")
default:
fmt.Println("Good evening.")
}
}
defer
与其他语句不同的语句,defer 语句会将函数推迟到外层函数返回之后执行。
推迟调用的函数其参数会立即求值,但直到外层函数返回前该函数都不会被调用
func main() {
defer fmt.Println("world")
fmt.Println("hello")
}
hello
world
推迟的函数会被压入到一个栈中,之后会按照后进先出的顺序调用
func main() {
fmt.Println("counting")
for i := 0; i < 3; i++ {
defer fmt.Println(i)
}
fmt.Println("done")
}
counting
done
2
1
0
其他类型
指针
使用上与c语言类似,不过没有指针运算
func main() {
i, j := 42, 2701
p := &i // point to i
fmt.Println(*p) // read i through the pointer
*p = 21 // set i through the pointer
fmt.Println(i) // see the new value of i
p = &j // point to j
*p = *p / 37 // divide j through the pointer
fmt.Println(j) // see the new value of j
}
结构体
使用type关键字声明结构体,可以使用大括号进行初始化
type Vertex struct {
X int
Y int
}
func main() {
fmt.Println(Vertex{1, 2})
}
与c类似,使用点号.
来访问结构体字段。有一个指向结构体的指针 p ,那么可以通过 (*p).X
来访问其字段 X ,与c不同的是,这种情况不是简写成p->X
,而是仍然可以写成p.X
func main() {
v := Vertex{1, 2}
p := &v
p.X = 1e9
fmt.Println(v)
}
{1000000000 2}
创建结构体实例的时候,可以通过一一对应进行初始化(数量与字段数量相同),也可以使用键值对(类似于python的字典)初始化部分字段
var (
v1 = Vertex{1, 2} // has type Vertex
v2 = Vertex{X: 1} // Y:0 is implicit
v3 = Vertex{} // X:0 and Y:0
p = &Vertex{1, 2} // has type *Vertex
)
func main() {
fmt.Println(v1, p, v2, v3)
}
{1 2} &{1 2} {1 0} {0 0}
数组
声明的时候在类型前使用方括号定义长度
func main() {
var a [2]string
a[0] = "Hello"
a[1] = "World"
fmt.Println(a[0], a[1])
fmt.Println(a)
primes := [6]int{2, 3, 5, 7, 11, 13}
fmt.Println(primes)
}
切片
当去掉声明数组中的长度时,该类型为切片。
最基本的语法类似于python中的切片,使用[low:high]
生成切片,但是不可以是负数,包括low元素,不包过high元素,所以high最多可以为数组的长度。切片的时候也可以忽略上下界,下界的默认值为0,上届的默认值则为该切片的长度。
func main() {
primes := [6]int{2, 3, 5, 7, 11, 13}
var s []int = primes[1:4]
fmt.Println(s)
fmt.Println(s[:2])
fmt.Println(s[1:])
fmt.Println(s[:])
}
[3 5 7]
[3 5]
[5 7]
[3 5 7]
Go中的切片与python中的切片最大的不同就是,切片并没有生成新的数据,它更像是数组部分数据的引用,修改切片会修改其相对应数组的元素
func main() {
names := [4]string{
"John",
"Paul",
"George",
"Ringo",
}
fmt.Println(names)
a := names[0:2]
b := names[1:3]
fmt.Println(a, b)
b[0] = "XXX"
fmt.Println(a, b)
fmt.Println(names)
}
[John Paul George Ringo]
[John Paul] [Paul George]
[John XXX] [XXX George]
[John XXX George Ringo]
Go中的切片会对应一个底层的数组,除了长度,它还拥有一个属性容量 切片的长度就是它所包含的元素个数。 切片的容量是从它的第一个元素开始数,到其底层数组元素末尾的个数。 切片 s 的长度和容量可通过表达式 len(s) 和 cap(s) 来获取。 切片操作时也可以有第三个参数,但是不是像python中的步长,而是定义切片的容量
func main() {
s := []int{2, 3, 5, 7, 11, 13}
printSlice(s)
// Slice the slice to give it zero length.
s = s[:0]
printSlice(s)
// Extend its length.
s = s[:4]
printSlice(s)
// Drop its first two values.
s = s[2:]
printSlice(s)
// shrink slice's cap
s = s[:len(s):cap(s) - 1]
printSlice(s)
}
func printSlice(s []int) {
fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}
len=6 cap=6 [2 3 5 7 11 13]
len=0 cap=6 []
len=4 cap=6 [2 3 5 7]
len=2 cap=4 [5 7]
len=2 cap=3 [5 7]
切片的零值是nil
,是长度和容量均为0,且没有底层数组
func main() {
var s []int
fmt.Println(s, len(s), cap(s))
if s == nil {
fmt.Println("nil!")
}
}
也可以通过内建函数make
来创建切片,三个参数分别为切片类型,长度,容量
func main() {
a := make([]int, 5)
printSlice("a", a)
b := make([]int, 0, 5)
printSlice("b", b)
c := b[:2]
printSlice("c", c)
d := c[2:5]
printSlice("d", d)
}
func printSlice(s string, x []int) {
fmt.Printf("%s len=%d cap=%d %v\n",
s, len(x), cap(x), x)
}
a len=5 cap=5 [0 0 0 0 0]
b len=0 cap=5 []
c len=2 cap=5 [0 0]
d len=3 cap=3 [0 0 0]
切片可包含任何类型,包括其他切片,类似于二维数组
func main() {
// Create a tic-tac-toe board.
board := [][]string{
[]string{"_", "_", "_"},
[]string{"_", "_", "_"},
[]string{"_", "_", "_"},
}
// The players take turns.
board[0][0] = "X"
board[2][2] = "O"
board[1][2] = "X"
board[1][0] = "O"
board[0][2] = "X"
for i := 0; i < len(board); i++ {
fmt.Printf("%s\n", strings.Join(board[i], " "))
}
}
X _ X
O _ X
_ _ O
内建函数append
可以向切片后追加元素,函数原型为func append(s []T, vs ...T) []T
,当切片的底层数组容量不足时,会分配一个更大的数组,返回的切片会指向新的数组
func main() {
var s []int
printSlice(s)
// append works on nil slices.
s = append(s, 0)
printSlice(s)
// The slice grows as needed.
s = append(s, 1)
printSlice(s)
// We can add more than one element at a time.
s = append(s, 2, 3, 4)
printSlice(s)
}
func printSlice(s []int) {
fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}
len=0 cap=0 []
len=1 cap=1 [0]
len=2 cap=2 [0 1]
len=5 cap=6 [0 1 2 3 4]
对于切片的遍历除了,除了一般的使用下标遍历外也可以使用range形式遍历,类似于python的遍历,每次迭代返回下标与下标所对应元素的副本。这两个值也可以通过使用_
来忽略,只需要下标也可以直接去掉value。
var pow = []int{1, 2, 4, 8, 16, 32, 64, 128}
func main() {
for i, v := range pow {
fmt.Printf("2**%d = %d\n", i, v)
}
}
2**0 = 1
2**1 = 2
2**2 = 4
2**3 = 8
map
map类型的定义方式与其他语言不同,使用map[key类型]value类型
进行定义。
map的零值也为nil
,既没有键也不能添加键。
获取和修改元素与python字典相同,删除元素使用delete
内建函数。
如果不存在某个键,则获取的是零值,要想判断某个键是否存在,可以通过双赋值进行检测elem, ok = m[key]
func main() {
m := map[string]int {
"Bill": 32,
}
fmt.Println("The map:", m)
m = make(map[string]int)
m["Answer"] = 42
fmt.Println("The value:", m["Answer"])
m["Answer"] = 48
fmt.Println("The value:", m["Answer"])
delete(m, "Answer")
fmt.Println("The value:", m["Answer"])
v, ok := m["Answer"]
fmt.Println("The value:", v, "Present?", ok)
}
The map: map[Bill:32]
The value: 42
The value: 48
The value: 0
The value: 0 Present? false
函数
函数也可以作为返回值以及参数,这点与js,python比较相似,也会有一个闭包的概念,也就是说通过闭包可以访问其他函数体内的局部变量,并且每个闭包实例拥有不同的独立的环境
func adder() func(int) int {
sum := 0
return func(x int) int {
sum += x
return sum
}
}
func main() {
pos, neg := adder(), adder()
for i := 0; i < 3; i++ {
fmt.Println(
pos(i),
neg(-2*i),
)
}
}
0 0
1 -2
3 -6
方法和接口
方法
Go中没有类,但可以为结构体定义方法,方法就是一类带特殊的接收者参数的函数,方法接收者位于func关键字与方法名之间。这样就可以类似于其他语言,使用方法接收者调用方法。
type Vertex struct {
X, Y float64
}
func (v Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
func main() {
v := Vertex{3, 4}
fmt.Println(v.Abs())
}
也可以为结构体以外的类型声明方法,但是只能为同一包内的类型作为接收者声明方法,所以无法对内建类型声明方法,需要先使用type
定义为自定类型
type MyFloat float64
func (f MyFloat) Abs() float64 {
if f < 0 {
return float64(-f)
}
return float64(f)
}
func main() {
f := MyFloat(-math.Sqrt2)
fmt.Println(f.Abs())
}
方法接收者也可以是指针,这样做的用处是可以修改接收者,否则只是在副本上做操作,类似于c语言中值传递和地址传递的区别。也可以避免每次调用时都进行复制,更加高效
type Vertex struct {
X, Y float64
}
func (v Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
func (v *Vertex) Scale(f float64) {
v.X = v.X * f
v.Y = v.Y * f
}
func main() {
v := Vertex{3, 4}
v.Scale(10)
fmt.Println(v.Abs())
}
50
与函数参数不同的是,方法接收者的指针会根据接收者类型进行重定向,而函数参数必须类型对应。也就是不管接收者是不是指针类型,实际调用时都会根据接收者类型解释为(&p).func()
,p.func()
或(*p).func()
进行正常的调用
type Vertex struct {
X, Y float64
}
func (v Vertex) Hypotenuse () float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
func (v *Vertex) Scale(f float64) {
v.X = v.X * f
v.Y = v.Y * f
}
func main() {
v := Vertex{3, 4}
fmt.Println(v.Hypotenuse ())
p := &Vertex{4, 3}
fmt.Println(p.Hypotenuse ())
v = Vertex{3, 4}
v.Scale(2)
p = &Vertex{4, 3}
p.Scale(3)
fmt.Println(v, p)
}
5
5
{6 8} &{12 9}
接口
接口类型是由一组方法签名定义的集合,接口类型的值可以保存任何实现了这些方法的值。类型通过实现一个接口的所有方法来实现接口,无需显示声明,无“implements”关键字。接口的值保存了一个具体底层类型的具体值,接口值调用方法会执行底层类型的同名方法。
type Abser interface {
Abs() float64
}
func main() {
var a Abser
f := MyFloat(-math.Sqrt2)
a = f // a MyFloat 实现了 Abser
fmt.Println(a.Abs())
describe(a)
}
type MyFloat float64
func (f MyFloat) Abs() float64 {
if f < 0 {
return float64(-f)
}
return float64(f)
}
func describe(i Abser) {
fmt.Printf("(%v, %T)\n", i, i)
}
1.4142135623730951
(-1.4142135623730951, main.MyFloat)
指定了另个方法的接口值称为空接口,空接口可保存任何类型的值,可以被用来处理未知类型的值,例如fmt.Print
。
func main() {
var i interface{}
describe(i)
i = 42
describe(i)
i = "hello"
describe(i)
}
func describe(i interface{}) {
fmt.Printf("(%v, %T)\n", i, i)
}
(<nil>, <nil> )
(42, int)
(hello, string)
接口类型可以进行断言,t := i.(T)
断言接口值i保存的类型T,并将底层类型为T的值赋予t,若i并未保存T类型的值,该语句会触发一个panic。也可以通过双赋值来进行判断,类似于判断map中是否包含某个键
func main() {
var i interface{} = "hello"
s := i.(string)
fmt.Println(s)
s, ok := i.(string)
fmt.Println(s, ok)
f, ok := i.(float64)
fmt.Println(f, ok)
f = i.(float64) // panic
fmt.Println(f)
}
hello
hello true
0 false
panic: interface conversion: interface {} is string, not float64
还可以使用i.(type)
进行类型选择
func do(i interface{}) {
switch v := i.(type) {
case int:
fmt.Printf("Twice %v is %v\n", v, v*2)
case string:
fmt.Printf("%q is %v bytes long\n", v, len(v))
default:
fmt.Printf("I don't know about type %T!\n", v)
}
}
func main() {
do(21)
do("hello")
}
Twice 21 is 42
"hello" is 5 bytes long
I dont know about type bool!
错误
Go程序使用error值来表示错误状态,error类型是一个内建接口,方法返回类型为string。通常函数会返回一个error值,可以通过判断是不是nil来进行错误处理,error为nil表示成功
Reader
io 包指定了 io.Reader 接口, 它表示从数据流的末尾进行读取。Go 标准库包含了该接口的许多实现, 包括文件、网络连接、压缩和加密等等。
io.Reader 接口有一个 Read 方法:func (T) Read(b []byte) (n int, err error)
Read 用数据填充给定的字节切片并返回填充的字节数和错误值。 在遇到数据流的结尾时,它会返回一个 io.EOF 错误。
func main() {
r := strings.NewReader("Hello, Reader!")
b := make([]byte, 8)
for {
n, err := r.Read(b)
fmt.Printf("n = %v err = %v b = %v\n", n, err, b)
fmt.Printf("b[:n] = %q\n", b[:n])
if err == io.EOF {
break
}
}
}
n = 8 err = <nil> b = [72 101 108 108 111 44 32 82]
b[:n] = "Hello, R"
n = 6 err = <nil> b = [101 97 100 101 114 33 32 82]
b[:n] = "eader!"
n = 0 err = EOF b = [101 97 100 101 114 33 32 82]
b[:n] = ""
并发
goroutine(go程)
goroutine是有Go在运行时管理的轻量级线程,通过go f(x,y,z)
启动新的goroutine并执行。f,x,y,z的求值是在当前goroutine中,而f的执行是在新的goroutine中
channels(信道)
channels是带有类型的管道,你可以通过信道操作符<-
来发送或者接受值
ch <- v // 将 v 发送至信道 ch。
v := <-ch // 从 ch 接收值并赋予 v。
与slice和map一样,信道在使用前需要通过make
创建。信道也可以是带缓冲的,将缓冲长度作为第二个参数提供给make
:
ch := make(chan int, 2)
默认情况下,发送和接收操作在另一端准备好之前都会阻塞。这使得 Go 程可以在没有显式的锁或竞态变量的情况下进行同步。如果是带缓冲的信道,则是当信道的缓冲区满了,向其发送数据时才会阻塞;挡缓冲区为空时,接收方会阻塞。以下例子中,send 5的时候就会阻塞:
func send(c chan int) {
c <- 3
fmt.Println("send 3 finished.")
c <- 4
fmt.Println("send 4 finished.")
c <- 5
fmt.Println("send 5 finished.")
}
func main() {
ch := make(chan int, 2)
ch <- 1
ch <- 2
go send(ch)
fmt.Println(<-ch)
fmt.Println(<-ch)
for {
}
}
send 3 finished.
1
2
send 4 finished.
发送者可通过close
关闭一个信道表示发送者没有需要发送的值了。接收者可以通过第二个接收参数来测试信道是否被关闭。
可以使用range
不断的从信道接收值,直到它被关闭
- 只有发送者可以关闭信道,向一个已经关闭的信道发送数据会引发panic
- 信道与文件无关,一般情况下无需关闭他们。只有必须告诉接收者不再有值需要发送的时候才要关闭,比如终止
range
循环
func fibonacci(n int, c chan int) {
x, y := 0, 1
for i := 0; i < n; i++ {
c <- x
x, y = y, x+y
}
close(c)
}
func main() {
c := make(chan int, 3)
go fibonacci(cap(c), c)
for i := range c {
fmt.Println(i)
}
c = make(chan int, 4)
go fibonacci(cap(c), c)
for {
v, ok := <- c
fmt.Println(v, ok)
if !ok {
break
}
}
}
0
1
1
0 true
1 true
1 true
2 true
0 false
select语句
select
语句使一个goroutine可以等待多个通信操作,select
会阻塞到某个分支可以继续执行位置,若多个分支都准备好,则随机选择一个执行
func fibonacci(c, quit chan int) {
x, y := 0, 1
for {
select {
case c <- x:
x, y = y, x+y
case <-quit:
fmt.Println("quit")
return
}
}
}
func main() {
c := make(chan int)
quit := make(chan int)
go func() {
for i := 0; i < 5; i++ {
fmt.Println(<-c)
}
quit <- 0
}()
fibonacci(c, quit)
}
0
1
1
2
3
quit
也可以为select
添加default
分支,这样当其他分支都没有准备好时,就会执行defult
分支
func main() {
tick := time.Tick(100 * time.Millisecond)
boom := time.After(300 * time.Millisecond)
for {
select {
case <-tick:
fmt.Println("tick.")
case <-boom:
fmt.Println("BOOM!")
return
default:
fmt.Println(" .")
time.Sleep(50 * time.Millisecond)
}
}
}
.
.
tick.
.
.
tick.
.
.
tick.
BOOM!
互斥锁
当不需要通信,只需要共享变量的时候,可以使用互斥锁sync.Mutex
以及两个方法Lock
和Unlock
// SafeCounter 的并发使用是安全的。
type SafeCounter struct {
v map[string]int
mux sync.Mutex
}
// Inc 增加给定 key 的计数器的值。
func (c *SafeCounter) Inc(key string) {
c.mux.Lock()
// Lock 之后同一时刻只有一个 goroutine 能访问 c.v
c.v[key]++
c.mux.Unlock()
}
// Value 返回给定 key 的计数器的当前值。
func (c *SafeCounter) Value(key string) int {
c.mux.Lock()
// Lock 之后同一时刻只有一个 goroutine 能访问 c.v
defer c.mux.Unlock()
return c.v[key]
}
func main() {
c := SafeCounter{v: make(map[string]int)}
for i := 0; i < 400; i++ {
go c.Inc("somekey")
}
time.Sleep(time.Second)
fmt.Println(c.Value("somekey"))
}
利用struct
的匿名字段(EmbeddedField)更方便的对一个结构体使用互斥锁,可以直接通过struct
进行锁的操作
var hits struct {
sync.Mutex
n int
}
hits.Lock()
hits.n++
hits.Unlock()
实际开发踩坑
MIT 6.824 Lab
-
环境部署
在安装第三方包的时候,会需要
https://golang.org/x/tools
包,但是这个网站被墙了。可以直接从github上下载镜像go get github.com/golang/tools
,把github.com/golang/tools
拷贝到golang.org/x
中做缓存 -
文件读取
- 使用
ioutil.ReadAll
是一个比较高效读取文件全部内容的办法 defer
在关闭文件流里是一个比较好的使用场景,可以更容易与open
配对- []byte可以直接通过类型转换转换为
string
- 在
ubuntu
下实验时发现flag参数中需要写flag才可以写内容,windows
下则不需要
file, _ := os.OpenFile(inFile, os.O_RDONLY, 0755) defer file.Close() content, _ := ioutil.ReadAll(file) keyValue := mapF(inFile, string(content))
- 使用
bufio
中的Reader.ReadLine("\n")
和Scanner
可以方便的按行读取
scanner := bufio.NewScanner(os.Stdin) // 默认按行分割 for scanner.Scan() { fmt.Println(scanner.Text()) }
- 使用
-
json
Go提供了相应的包
encoding/json
,可以方便的编码与解析json字符串file, _ = os.OpenFile(reduceName(jobName, mapTaskNumber, i), os.O_CREATE | os.O_TRUNC, 0755) defer file.Close() enc = json.NewEncoder(file) for _, kv := range keyValue { enc.Encode(&kv) } dec := json.NewDecoder(file) for { var kv KeyValue if err := dec.Decode(&kv); err == io.EOF { break } else if err != nil { log.Fatal(err) } log.Println(kv) }
-
语法
- slice和map不能声明后直接使用,此时为零值
nil
,要再声明时初始化或者使用make
var keyValues map[string]string map["key"] = "value" // panic:assignment to entry in nil map keyValues := make(map[string]string) map["key"] = "value" // correct
- 很多函数会有多个返回值,比如错误信息。如果只需要部分返回值,可以使用
_
进行占位,for
循环中也会使用到
file, _ := os.OpenFile(reduceName(jobName, i, reduceTaskNumber), os.O_RDONLY, 0755) for _, kv := range keyValue { r := ihash(kv.Key) % nReduce encs[r].Encode(&kv) }
- 与js类似,也会存在闭包中序号使用错误的问题,不过不会像js的setTImeout按顺序的加入到loopEvent中,gorutine是不会保证顺序的 ``` go for i := 0; i < 5; i++ { go func() { fmt.Println(i) }() }
for i := 0; i < 5; i++ { go func(i int) { fmt.Printf(“%v “, i) }(i) }
* slice的[:]的值改变后,依然会影响slice,因为底层对应的数组没有变化 ``` go var slice = []int{1, 2, 3, 4} sliceCopy := slice[:] sliceCopy[0] = 0 fmt.Println(slice) [0 2 3 4]
- 可以通过select和channel来结束goroutine,类似与通过flag来结束无限循环
go func() { for { select { case <-quitCh: return default: // do something } } }() close(quitCh)
- slice和map不能声明后直接使用,此时为零值
- RPC
- RPC handle中的参数和返回结构体所有的field的首字母需要大写,包括子结构体的field
-
实现逻辑
- 实现把文本分割为word的逻辑错误:之前是直接通过
strings.Fields
(按照空白字符)分割,再检查分割好的小块是否每个字符都是字母,这样不仅结果错误,而且效率很低;可以按照非字母字符分割(FieldsFunc
),所分割好的就都是word了
// correct f := func(c rune) bool { return !unicode.IsLetter(c) } words := strings.FieldsFunc(contents, f) var keyValues []mapreduce.KeyValue for _, word := range words { keyValues = append(keyValues, mapreduce.KeyValue{word, strconv.Itoa(1)}) } // error words := strings.Fields(contents, f) var keyValues []mapreduce.KeyValue for _, word := range words { var isWord = true for _, r := range word { if !unicode.IsLetter(r) { isWord = false } } if isWord { keyValues = append(keyValues, mapreduce.KeyValue{word, strconv.Itoa(1)}) } }
- Lab2B中,leader的commitIndex的更新的逻辑错误:之前是类似于选举过程,有一半以上的server成功复制后才更新commitIndex到lastIndex;commitIndex更新逻辑应该是在已经更新的index中寻找合适index(排序,超过大部分server的index),与当前有几个follower正常复制并回复无关。
在goroutine中利用wiatGroup,在所有server得到回复后再更新;要注意leader自己的matchIndex也应该在新的command提交时更新 - apply Command的时候,为了避免阻塞,起了一个新的goroutine,在goroutine中applyCh <- Command。这个操作是没有必要的,本身Command的序列顺序是严格不变的,就是应该串行的。
- Lab2C中,测试用例中可能会报goroutine超过限制的错误,有两个地方导致了goroutine不断增加:1.实验中的测试用例销毁server,重建并重新加入到集群的操作不会销毁之前server的gorutine,需要在Kill()中结束不需要的routine;2.之前提到的更新commitIndex的逻辑有问题,尤其在网络不好的时候,会造成多个goroutine在WaitGroup.wait()的时候阻塞,大大增加goroutine的数量。commitIndex的更新不需要局限于当前心跳过程中得到所有server的回复后再更新,只需要每次心跳根据当前[]matchIndex的状态更新就好,不需要启新的goroutine
- len(channel)是不够安全的,不能保证是按照预期的。Lab2C中leader发送heartBeats是并发的发送到所有其他server,当reply的term大于当前term时会退化到follower并更新term,此时可能会造成其他的heartBeats正常发送。之前在回退时时通过向channel发送值,其他goroutine通过len(channel)来判断是否已经回退,这种办法还是无法避免上述的问题。修改为直接通过state来判断是否回退,在跑测试用例时没有在碰到过这种问题
- 在Lab3A中range channel时,遇到重复的Command跳出循环了,造成发送方堵塞(raft),应该continue继续循环等待接收
- 实现把文本分割为word的逻辑错误:之前是直接通过