Go学习笔记

基础部分根据官网的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不断的从信道接收值,直到它被关闭

  1. 只有发送者可以关闭信道,向一个已经关闭的信道发送数据会引发panic
  2. 信道与文件无关,一般情况下无需关闭他们。只有必须告诉接收者不再有值需要发送的时候才要关闭,比如终止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以及两个方法LockUnlock

// 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

  1. 环境部署

    在安装第三方包的时候,会需要 https://golang.org/x/tools包,但是这个网站被墙了。可以直接从github上下载镜像go get github.com/golang/tools,把github.com/golang/tools拷贝到golang.org/x中做缓存

  2. 文件读取

    • 使用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())
         }
    
  3. 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)
     }
    
  4. 语法

    • 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)
    
  5. RPC
    • RPC handle中的参数和返回结构体所有的field的首字母需要大写,包括子结构体的field
  6. 实现逻辑

    • 实现把文本分割为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继续循环等待接收

odroid搭建Android实验环境过程

Published on September 18, 2018

Java并发编程知识总结

Published on August 26, 2018