0 参考资料

GO开源项目收集https://github.com/avelino/awesome-go

01 第一章 Go语言简介

1.1 工作区和GOPATH

工作区是放置Go源码文件的目录
工作区下边有pkg、src、bin三个目录

  • pkg目录

用于存放归档文件(名称以.a为后缀的文件),所有归档文件都会被存放到该目录下的平台相关目录中,同样以代码包为组织形式

  • bin目录

用于存放当前工作区中的Go程序的可执行文件

  1. 当环境变量GOBIN已有效设置时,该目录会变的无意义
  2. 当GOPATH的值中包含多个工作区的路径时,必须设置GOBIN,否则无法成功安装Go程序的可执行文件

1.2 代码包的导入

  • 本地化导入
1
2
3
import . "strings"

HasPrefix("abc", "a") //导入后使用函数不需要加包名

1.3 包管理

1.3.1 Go的依赖管理

1.3.2 go mod

1
2
3
4
5
6
7
8
go mod init mod名称
go build 源码文件
go mod download 下载依赖包
go mod tidy 更新go.mod依赖记录
go mod verify 校验go.mod文件或依赖包源码
go mod why 查看依赖信息,为什么依赖
go mod edit
go mod vendor
  • go mod edit 修改mod信息
1
2
3
4
5
6
7
go mod edit -module test
go mod edit -require github.com/hashicorp/golang-lru@0.5.3 添加依赖包到mod
go mod edit -fmt 格式化mod文件
go mod edit -exclude 排除某个依赖包
go mod edit -exclude github.com/hashicorp/golang-lru@0.5.2 不能依赖此包
go mod edit -dropexclude github.com/hashicorp/golang-lru@0.5.2 删除go.mod中的配置

  • go.mod
1
2
3
4
5
module baihl

go 1.15

exclude github.com/hashicorp/golang-lru 0.5.2
  • go mod vendor
1
go mod vendor 生成vendor目录存放依赖包

1.4 总结

  • 列出依赖
1
2
3
go mod graph
go mod why
go list -m all
  • 添加依赖
1
2
3
4
go get 
go build
go mod edit require
go mod download

02 第二章 基本程序结构

2.1 退出返回值

与其他语言不同,Go中main函数不支持任何返回值
通过os.Exit来返回状态

1
2
3
4
5
6
func main() {
if len(os.Args) > 1 {
fmt.Println("Hello World", os.Args[1])
}
os.Exit(2)
}

输出:

1
Process finished with exit code 2

2.2 获取命令行参数

与其他语言不同,main函数不支持传入参数
在程序中使用os.Args获取命令行参数

1
2
3
4
5
func main() {
if len(os.Args) > 2 {
fmt.Println("Hello World", os.Args[1], os.Args[2])
}
}

输出:

1
2
#go run hello_world.go bai haoliang
Hello World bai haoliang

2.3 编写测试程序

  1. 源码文件以_test结尾:XXX_test.go
  2. 测试方法名以Test开头:func TestXXX(t *testing.T) {...}
1
2
3
4
5
6
7
8
9
10
11
func TestFibList(t *testing.T) {
a := 1
b := 1
t.Log(a)
for i := 0; i < 5; i++ {
t.Log(" ", b)
tmp := a
a = b
b = tmp + a
}
}

2.4 变量赋值

  1. 赋值可以进行自动类型推断
  2. 在一个赋值语句中可以对多个变量进行同时赋值
1
2
3
var b int = 1
a := 1
c, d := 1, 2

2.5 常量的定义

快速设置连续值

1
2
3
4
5
6
7
8
9
const (
Monday = iota + 1
Tuesday
Wednesday
Thursday
Friday
Saturday
Sunday
)

按位设置

1
2
3
4
5
const (
Open = 1 << iota
Close
Pending
)

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package constant_test

import "testing"

const (
Monday = 1 + iota
Tuesday
Wednesday
)

const (
Readable = 1 << iota
Writable
Executable
)

func TestConstantTry(t *testing.T) {
t.Log(Monday, Tuesday)
}

func TestConstantTry1(t *testing.T) {
a := 1 //0001
t.Log(a&Readable == Readable, a&Writable == Writable, a&Executable == Executable)
}

2.5 常量的使用

const数值可作为各种类型使用

1
2
3
const filename = "abc.txt"
const a, b = 3, 4
var c int = int(math.Sqrt(a * a + b * b))

2.6 基本数据类型

  • 基本数据类型

  • 类型转化
  1. Go语言不允许隐式类型转换
  2. 别名和原有类型也不能进行隐式转换
1
2
3
4
5
6
7
8
9
10
11
12
type MyInt int64

func TestImplicit(t *testing.T) {
var a int32 = 1
var b int64
b = int64(a)
var c MyInt
c = MyInt(b)
//b = c // 隐式类型转换 error
t.Log(a, b, c) // 1 1 1
t.Log(math.MaxFloat32)
}
  • 基本的预定义值
  1. math.MaxInt64
  2. math.MaxFloat64
  3. math.MaxUint32
  • 指针类型
  1. 不支持指针运算
  2. string 是值类型,其默认的初始化为空字符串,而不是nil
1
2
3
4
5
6
7
8
9
10
11
12
13
14
func TestPoint(t *testing.T) {
a := 1
aPtr := &a
//aPtr = aPtr + 1
t.Log(a, aPtr) // 1 0xc0000a6100
t.Logf("%T %T", a, aPtr) // int *int
}

func TestString(t *testing.T) {
var s string
t.Log("*" + s + "*") //初始化零值是“” **
t.Log(len(s)) // 0

}
  • 定义指针类型
1
mMap := new(map[int]string)

2.6.1 指针/数组指针/指针数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func TestPoint() {
var count int = 20
var countPoint *int
countPoint = &count
fmt.Println("count 的地址 : ", countPoint)
}

func TestPointArr() {
//指针数组
a, b := 1, 2
pointArr := [...]*int{&a, &b}
fmt.Println("指针数组 pointArr : ", pointArr)

//数组指针
arr := [...]int{3, 4, 5}
arrPoint := &arr
fmt.Println("数组指针 arrPoint : ", arrPoint)
}

func main() {
TestPoint()
TestPointArr()
}

输出

1
2
3
count 的地址 :  0xc000012090
指针数组 pointArr : [0xc000012098 0xc0000120b0]
数组指针 arrPoint : &[3 4 5]

2.6.2 指针使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package main

import (
"fmt"
"math"
)

type Vertex struct {
X, Y float64
}

func (v Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

//通过指针修改Vertex中的值
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())
}

2.6.2 结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
type Dog struct {
ID int
Name string
Age int
}

func main() {
dog := new(Dog) //定义指针类型
dog.ID = 1
dog.Name = "Tom"
dog.Age = 12
fmt.Println(dog)
}

2.7 算术运算符

注意: Go语言没有前置的++,–,如(++a)

2.8 比较运算符

可以使用==比较数组

  1. 相同维数且含有相同个数元素的数组才可以比较
  2. 每个元素都相同的才相等
1
2
3
4
5
6
7
8
9
func TestCompareArray(t *testing.T) {
a := [...]int{1, 2, 3, 4}
b := [...]int{1, 3, 2, 4}
// c := [...]int{1, 2, 3, 4, 5}
d := [...]int{1, 2, 3, 4}
t.Log(a == b) // false
//t.Log(a == c)
t.Log(a == d) // true
}

2.9 逻辑运算符

2.10 位运算符

Go语言多了一种位运算符 &^ (按位置零)

1
2
3
4
1&^0-- 1 
1&^1-- 0
0&^1-- 0
0&^0-- 0

把指定位置零

示例:

1
2
3
4
5
6
7
8
9
10
11
12
const (
Readable = 1 << iota
Writable
Executable
)

func TestBitClear(t *testing.T) {
a := 7 //0111
a = a &^ Readable
a = a &^ Executable
t.Log(a&Readable == Readable, a&Writable == Writable, a&Executable == Executable)
}

输出

1
false true false

2.11 循环语句

与其他语言不同
只有for没有while语句

1
2
3
4
5
6
7
8
func TestWhileLoop(t *testing.T) {
n := 0
for n < 5 {
t.Log(n)
n++
}
}

2.12 条件语句

2.12.1 if 条件

与其他语言的差异

  1. condition表达式结果必须为布尔值
  2. 支持变量赋值:
1
2
3
if var declaration; condition {
//code to be executed if condition is true
}

2.12.2 switch条件

  1. 条件表达式不限制为常量或者整数
  2. 单个case中,可以出现多个结果,使用逗号分隔
1
2
3
4
5
6
7
8
9
10
11
12
func TestSwitchMultiCase(t *testing.T) {
for i := 0; i < 5; i++ {
switch i {
case 0, 2:
t.Log("Even")
case 1, 3:
t.Log("Odd")
default:
t.Log("it is not 0-3")
}
}
}
  1. 与C语言等规则相反,Go语言不需要用break来明确退出一个case

  2. 可以不设定switch之后的条件表达式,在此种情况下,整个switch结构与多个if…else…的逻辑作用等同

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    func TestSwitchCaseCondition(t *testing.T) {
    for i := 0; i < 5; i++ {
    switch {
    case i%2 == 0:
    t.Log("Even")
    case i%2 == 1:
    t.Log("Odd")
    default:
    t.Log("unknow")
    }
    }
    }
  3. fallthrough

使用 fallthrough 会强制执行后面的 case 语句,fallthrough 不会判断下一条 case 的表达式结果是否为 true。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main

import "fmt"

func main() {

    switch {
    case false:
            fmt.Println("1、case 条件语句为 false")
            fallthrough
    case true:
            fmt.Println("2、case 条件语句为 true")
            fallthrough
    case false:
            fmt.Println("3、case 条件语句为 false")
            fallthrough
    case true:
            fmt.Println("4、case 条件语句为 true")
    case false:
            fmt.Println("5、case 条件语句为 false")
            fallthrough
    default:
            fmt.Println("6、默认 case")
    }
}

以上代码执行结果为:

1
2
3
2、case 条件语句为 true
3、case 条件语句为 false
4、case 条件语句为 true

switch 从第一个判断表达式为 true 的 case 开始执行,如果 case 带有 fallthrough,程序会继续执行下一条 case,且它不会去判断下一个 case 的表达式是否为 true。

2.12.3 select语句

select语句只能用于通道。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import "fmt"

func main() {
ch4 := make(chan int, 1)
for i := 0; i < 4; i++ {
select {
case e, ok := <-ch4:
if !ok {
fmt.Println("End.")
return
}
fmt.Println(e)
close(ch4)
default:
fmt.Println("No Data!")
ch4 <- 1
}
}
}

03 常用集合

3.0 概览

3.1 数组

  • 数组的传递是值传递

  • [10]int和[20]int是不同的类型

  • 调用func f(arr [10]int) 会 拷贝 数组

  • 数组的定义和声明

    1
    2
    3
    4
    5
    6
    7
    8
    9
    func TestArrayInit(t *testing.T) {
    var arr [3]int //声明并初始化为默认零值
    arr1 := [4]int{1, 2, 3, 4}
    arr3 := [...]int{1, 3, 4, 5}、
    arr4 := [2]][2]int{{1, 2}, {3, 4}}
    arr1[1] = 5
    t.Log(arr[1], arr[2])
    t.Log(arr1, arr3)
    }
  • 数组元素遍历

1
2
3
4
5
6
7
8
9
func TestArrayTravel(t *testing.T) {
arr3 := [...]int{1, 3, 4, 5}
for i := 0; i < len(arr3); i++ {
t.Log(arr3[i])
}
for _, e := range arr3 {
t.Log(e)
}
}
  • 数组截取

截取时是左闭右开区间
如下:

1
2
3
4
5
6
a := [...]int{1,2,3,4,5}
a[1:2] // 2
a[1:3] // 2,3
a[1:len(a)] // 2,3,4,5
a[1:] // 2,3,4,5
a[:3] // 1,2,3

3.2 切片

  • 切片的定义
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func TestSliceInit(t *testing.T) {
var s0 []int
t.Log(len(s0), cap(s0))
s0 = append(s0, 1)
t.Log(len(s0), cap(s0))

s1 := []int{1, 2, 3, 4}
t.Log(len(s1), cap(s1))

s2 := make([]int, 3, 5)
t.Log(len(s2), cap(s2))
t.Log(s2[0], s2[1], s2[2])
s2 = append(s2, 1)
t.Log(s2[0], s2[1], s2[2], s2[3]) //0 0 0 1
t.Log(len(s2), cap(s2))
}

其中len个元素会被初始化为零值,未初始化元素不可访问,会出现编译错误

  • 切片中len和cap的关系

可以通过如下程序看出len和cap的关系

1
2
3
4
5
6
7
func TestSliceGrowing(t *testing.T) {
s := []int{}
for i := 0; i < 10; i++ {
s = append(s, i)
t.Log(len(s), cap(s))
}
}

可以发现随着len的长度空间不够,cap会以倍数增加。

1
2
3
4
5
6
7
8
9
10
slice_test.go:26: 1 1
slice_test.go:26: 2 2
slice_test.go:26: 3 4
slice_test.go:26: 4 4
slice_test.go:26: 5 8
slice_test.go:26: 6 8
slice_test.go:26: 7 8
slice_test.go:26: 8 8
slice_test.go:26: 9 16
slice_test.go:26: 10 16
  • 共享存储结构

切片的访问的数据可以与其他切片共享,如果被某个切片修改,则会影响其他切片

代码示例:

1
2
3
4
5
6
7
8
9
10
11
func TestSliceShareMemory(t *testing.T) {
year := []string{"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep",
"Oct", "Nov", "Dec"}
Q2 := year[3:6]
t.Log(Q2, len(Q2), cap(Q2))
summer := year[5:8]
t.Log(summer, len(summer), cap(summer))
summer[0] = "Unknow"
t.Log(Q2)
t.Log(year) //[Jan Feb Mar Apr May Unknow Jul Aug Sep Oct Nov Dec]
}
  • 两切片不可以直接比较
1
2
3
4
5
6
7
8
func TestSliceComparing(t *testing.T) {
a := []int{1, 2, 3, 4}
b := []int{1, 2, 3, 4}
// if a == b { //切片只能和nil比较
// t.Log("equal")
// }
t.Log(a, b) //[1 2 3 4] [1 2 3 4]
}

总结:

  1. 数组可以直接比较,容量固定,可以截取访问
  2. 切片不可以直接比较,容量可以自动扩充,可以直接访问len长度的元素,越界编译出错
  • 切片扩容

每一个切片都有一个底层数组,切片的底层数组什么时候会被替换?

确切地说,一个切片的底层数组永远不会被替换。为什么?虽然在扩容的时候 Go 语言一定会生成新的底层数组,但是它也同时生成了新的切片。它只是把新的切片作为了新底层数组的窗口,而没有对原切片,及其底层数组做任何改动。请记住,在无需扩容时,append函数返回的是指向原底层数组的新切片,而在需要扩容时,append函数返回的是指向新底层数组的新切片。

3.3 Map

  • 创建:make(map[string]int)

  • 获取元素:m[key]

  • key不存在时,获取value类型的初值

  • 用value, ok := m[key]来判断是否存在key

  • 用delete删除一个key

  • 使用range遍历key,或者遍历key,value对

  • 不保证遍历顺序,如需顺序,需手动对key排序

  • 使用len获得元素个数

  • map使用哈希表,必须可以比较相等

  • 除了slice,map,function的内建类型都可以作为key

  • struct类型不包含上述字段,也可作为key

  • Map的定义

1
2
3
4
5
6
7
8
9
10
func TestInitMap(t *testing.T) {
m1 := map[int]int{1: 1, 2: 4, 3: 9}
t.Log(m1[2])
t.Logf("len m1=%d", len(m1)) // m1=3
m2 := map[int]int{}
m2[4] = 16
t.Logf("len m2=%d", len(m2)) // m2=1
m3 := make(map[int]int, 10)
t.Logf("len m3=%d", len(m3)) // m3=0
}

map类型在使用make函数时只需要两个参数,第二个参数是map的cap长度

  • Map的遍历
1
2
3
4
5
6
func TestTravelMap(t *testing.T) {
m1 := map[int]int{1: 1, 2: 4, 3: 9}
for k, v := range m1 {
t.Log(k, v)
}
}
  • 判断Map的key是否存在
1
2
3
4
5
6
7
8
9
10
11
12
func TestAccessNotExistingKey(t *testing.T) {
m1 := map[int]int{}
t.Log(m1[1])
m1[2] = 0
t.Log(m1[2])
m1[3] = 0
if v, ok := m1[3]; ok { // 为true表示存在
t.Logf("Key 3's value is %d", v)
} else {
t.Log("key 3 is not existing.")
}
}
  • 删除Map中的某个元素
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func TestMapForSet(t *testing.T) {
mySet := map[int]bool{}
mySet[1] = true
mySet[3] = true
n := 3
if _, ok := mySet[n]; ok {
t.Logf("%d is existing", n)
} else {
t.Logf("%d is not existing", n)
}
mySet[3] = true
t.Log(len(mySet))
delete(mySet, 3) // 删除key为3的元素
if _, ok := mySet[n]; ok {
t.Logf("%d is existing", n)
} else {
t.Logf("%d is not existing", n)
}
}
  • Map的value可以是函数
1
2
3
4
5
6
7
func TestMapWithFunValue(t *testing.T) {
m := map[int]func(op int) int{}
m[1] = func(op int) int { return op }
m[2] = func(op int) int { return op * op }
m[3] = func(op int) int { return op * op * op }
t.Log(m[1](2), m[2](2), m[3](2)) //2 4 8
}
  • 练习:映射

统计字符串中每个单词的个数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import (
"fmt"
"strings"

"golang.org/x/tour/wc"
)

func WordCount(s string) map[string]int {
array_s := strings.Fields(s)
fmt.Println(array_s)
m := make(map[string]int)
for _, v := range array_s {
m[v] += 1
}

return m
}

func main() {
wc.Test(WordCount)
}

04 字符串

  1. string 是数据类型,不是引用或指针类型
  2. string 是只读的byte slice,len函数可以获取所含的byte数
  3. string 的byte数组可以存放任何数据
  • rune类型

string底层是通过byte数组实现,中文字符在unicode下占两个字节,在utf-8编码下占3个字节,goland中默认编码就是utf-8
rune类型可以处理unicode字符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func TestString(t *testing.T) {
var s string
t.Log(s) //初始化为默认零值“”
s = "hello"
t.Log(len(s))
//s[1] = '3' //string是不可变的byte slice
//s = "\xE4\xB8\xA5" //可以存储任何二进制数据
s = "\xE4\xBA\xBB\xFF"
t.Log(s)
t.Log(len(s))
s = "中"
t.Log(len(s)) //是byte数 3

c := []rune(s)
t.Log(len(c)) // 1
t.Logf("中 unicode %x", c[0])
t.Logf("中 UTF8 %x", s)
}

4.1 参考资料

string常用功能包

  1. strings包(https://golang.org/pkg/strings/)
  2. strconv包 (https://golang.org/pkg/strconv/)

示例:

  1. 字符串切割
1
2
3
4
5
6
7
8
func TestStringFn(t *testing.T) {
s := "A,B,C"
parts := strings.Split(s, ",")
for _, part := range parts {
t.Log(part)
}
t.Log(strings.Join(parts, "-"))
}
  1. 字符串与整型转换
    1
    2
    3
    4
    5
    6
    7
    func TestConv(t *testing.T) {  
    s := strconv.Itoa(10)
    t.Log("str" + s) // str10
    if i, err := strconv.Atoi("10"); err == nil {
    t.Log(10 + i) // 20
    }
    }

05 函数

  • 可以有多个返回值

  • 所有参数都是值传递:slice,map,channel会有引用的错觉

    因为slice是共享结构,所以在函数内修改后,函数外会感知

  • 函数可以作为变量的值

  • 函数可以作为参数和返回值

  • 函数作为参数的定义

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    type IntConv func(op int) int

    func timeSpent(inner IntConv) IntConv {
    return func(n int) int {
    start := time.Now()
    ret := inner(n)
    fmt.Println("time spent:", time.Since(start).Seconds())
    return ret
    }
    }

    func slowFun(op int) int {
    time.Sleep(time.Second * 1)
    return op
    }

    func TestFn(t *testing.T) {
    tsSF := timeSpent(slowFun)
    t.Log(tsSF(10))
    }

    函数作为参数,并且计算函数运行时间

5.1 可变参数

可变参数的函数定义

1
2
3
4
5
6
7
8
9
10
11
12
func Sum(ops ...int) int {
ret := 0
for _, op := range ops {
ret += op
}
return ret
}

func TestVarParam(t *testing.T) {
t.Log(Sum(1, 2, 3, 4))
t.Log(Sum(1, 2, 3, 4, 5))
}

可以定义接收任何类型的可变参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//可变参数...interface{}做为函数参数的类型判断
//1.在函数[actionVariables3],增加功能,判断可变参数的类型

func actionVariables3(args ...interface{}) {
for _,value := range args {
switch value.(type) {
case int:
fmt.Println(value,"int")
case string:
fmt.Println(value,"string")
case float64:
fmt.Println(value,"float64")
case bool:
fmt.Println(value,"bool")
default:
fmt.Println(value,"unknow")
}
}
}
//2.调用
actionVariables3(1,"我是波哥",3.14,true)

可变参数,类似于切片,go语言里有很多,可变参数是go语言的特性。

5.2 defer的使用

1
2
3
4
5
6
7
func TestDefer(t *testing.T) {
defer func() {
fmt.Println("Clear resources.")
}()
fmt.Println("Start")
panic("err")
}

defer可以延迟执行,但会在panic之前执行

1
2
3
4
5
Start
Clear resources.
--- FAIL: TestDefer (0.00s)
panic: err [recovered]
panic: err

5.3 递归函数

1
2
3
4
5
6
7
8
9
10
11
12
- 1.定义函数[fibonaci]实现

func fibonaci(index int64) int64 {
if index == 1 || index == 2 {
return 1
} else {
return fibonaci(index-1) + fibonaci(index-2)
}
}
- 2.调用
//第20位置对应的值
fmt.Println(fibonaci(2))

5.4 匿名函数

没有名字的函数被称为匿名函数,当一个函数仅使用一次的时候,可定义为匿名函数

  1. 使用方式一

定义一个匿名函数实现两个数的加和,定义的时候并调用。

1
2
3
4
sumVariables := func(var1,var2 int) int {
return var1 + var2
}(1,2)
fmt.Println(sumVariables)
  1. 使用方式二
1
2
3
4
5
//将一个匿名函数赋值给一个变量,这个变量存储的是这个匿名的地址
func1 := func(var1,var2 int) int{
return var1 - var2
}
fmt.Printf("func1调用的值=%d,func1=%p\n",func1(1,2),&func1)

输出

1
func1调用的值=-1,func1=0xc00008c000

5.5 闭包

首先它是一个函数,是函数与函数外部数据的引用(在函数内部引用了函数体外部的数据)

1
2
3
4
5
6
7
8
9
10
11
12
13
func clousure() func(int64) int64 {
var step int64 = 0
return func(_step int64) int64 {
//函数内部引用函数外部的数据
step += _step
return step
}
}

func2 := clousure()
fmt.Println(func2(1))
fmt.Println(func2(2))
fmt.Println(func2(3))

输出:

1
2
3
1
3
6

07 错误处理

7.1 错误机制

  1. 没有异常机制
  2. error类型实现了error接口
  3. 可以通过errors.New来快速创建错误实例

errors.New("n must be in the range [0,100]")

通过errors定义不同的错误变量,以便于判断错误类型

7.2 panic和recover

  • panic
  1. panic用于不可以恢复的错误
  2. panic退出前会执行defer指定的内容
  3. panic会打印出当前调用栈
  • panic vs os.Exit
  1. os.Exit退出时不会调用defer指定的函数
  2. os.Exit退出时不输出当前调用栈信息
  • 使用recover接收panic的错误
1
2
3
4
5
6
7
8
9
10
11
12
func TestPanicVxExit(t *testing.T) {

defer func() {
if err := recover(); err != nil {
fmt.Println("recovered from ", err)
}
}()
fmt.Println("Start")
panic(errors.New("Something wrong!"))
//os.Exit(-1)
//fmt.Println("End")
}

recover应该谨慎使用,因为recover可以捕捉panic错误,使程序不用退出,但如果捕捉后不做正确处理,可能形成僵尸程序,导致health check失效,还不如直接使用panic使程序退出,然后使用程序的守护进程重启程序。

7.3 异常处理示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package main

import (
"errors"
"fmt"
)

func receivePanic() {
defer coverPanic()
panic(errors.New("I am error"))
}

func coverPanic() {
message := recover()
switch message.(type) {
case string:
fmt.Println("string message : ", message)
case error:
fmt.Println("error message : ", message)
default:
fmt.Println("Unknown panic : ", message)
}
}

func main() {
receivePanic()
}

09 并发编程

9.1 协程机制

Thread vs. Groutine

  1. 创建时默认的stack大小
  • JDK5以后Java Thread stack默认为1M
  • Groutine的stack初始化大小为2K
  1. 和KSE(Kernel Space Entity)的对应关系
  • Java Thread 是1:1
  • Groutine是 M:N

Groutine的机制是在用户空间由用户控制线程的切换,又因为KSE中M:N的关系,不会造成内核空间切换

9.2 共享内存并发机制

  • Lock 和 WaitGroup
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func TestCounterWaitGroup(t *testing.T) {
var mut sync.Mutex
var wg sync.WaitGroup
counter := 0
for i := 0; i < 5000; i++ {
wg.Add(1)
go func() {
defer func() {
mut.Unlock()
}()
mut.Lock()
counter++
wg.Done()
}()
}
wg.Wait()
t.Logf("counter = %d", counter)

}

输出:

1
counter = 5000

通过加锁实现并发Groutine共享变量

9.3 CSP并发机制

Go中channel是有容量限制并且独立于处理Groutine
通过Channel控制并发:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func otherTask() {
fmt.Println("working on something else")
time.Sleep(time.Millisecond * 100)
fmt.Println("Task is done.")
}

func AsyncService() chan string {
retCh := make(chan string, 1)
//retCh := make(chan string, 1)
go func() {
ret := service()
fmt.Println("returned result.")
retCh <- ret
fmt.Println("service exited.")
}()
return retCh
}

func TestAsynService(t *testing.T) {
retCh := AsyncService()
otherTask()
fmt.Println(<-retCh)
time.Sleep(time.Second * 1)
}

9.4 多路选择和超时控制

通过select实现多路选择

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func ServiceTime() chan string {
retCh := make(chan string, 1)
time.Sleep(time.Millisecond * 50)
ret := "Done"
retCh <- ret
return retCh
}

func TestSelect(t *testing.T) {
select {
case <-time.After(time.Millisecond * 100): //100毫秒超时
t.Error("time out")
case ret := <-ServiceTime():
t.Log(ret)
}
}

可以通过case实现接收不同channel消息,然后执行相应case

9.5 Channel的关闭和广播

  • 向关闭的channel发送数据会导致panic
  • 读取关闭的channel,不会阻塞,返回是0
  • v, ok <- ch; ok为bool值,true表示正常接收,false表示通道关闭
  • 所有的channel接收者都会在channel关闭时,立即从阻塞等待中返回且上诉ok值为false,可以用此,想多个订阅者发送退出信号。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
func dataProducer(ch chan int, wg *sync.WaitGroup) {
go func() {
for i := 0; i < 10; i++ {
ch <- i
}
close(ch) //生产者发送完数据后关闭channel
wg.Done()
}()

}

func dataReceiver(ch chan int, wg *sync.WaitGroup) {
go func() {
for {
if data, ok := <-ch; ok {
fmt.Println(data)
} else {
break
}
}
wg.Done()
}()

}

func TestCloseChannel(t *testing.T) {
var wg sync.WaitGroup
ch := make(chan int)
wg.Add(1)
dataProducer(ch, &wg)
wg.Add(1)
dataReceiver(ch, &wg)
// wg.Add(1)
// dataReceiver(ch, &wg)
wg.Wait()
}

9.6 Context与任务取消

  • 根Context:通过context.Background()创建
  • 子Context:context.WithCancel(parentContext)创建
    • ctx, cancel := context.WithCancel(context.Background())
  • 当前Context被取消时,基于他的子context都会被取消
  • 接收取消通知 <-ctx.Done()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
func isCancelled(ctx context.Context) bool {
select {
case <-ctx.Done():
return true
default:
return false
}
}

func TestCancel(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
for i := 0; i < 5; i++ {
go func(i int, ctx context.Context) {
for {
if isCancelled(ctx) {
break
}
time.Sleep(time.Millisecond * 5)
}
fmt.Println(i, "Cancelled")
}(i, ctx)
}
cancel()
time.Sleep(time.Second * 1)
}

9.7 多协程间的通信

两个协程通过channel进行数据通信

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package main

import (
"fmt"
"runtime"
"time"
)

var ch1 chan int = make(chan int, 10)
var ch2 chan int = make(chan int, 10)

//从channel中读取数据
func Read() {

for {
select {
case num := <-ch1:
fmt.Println(num)
case <-ch2:
fmt.Println("ch2")
}
}
}

//向channel中写入数据
func Write() {
ch1 <- 1
time.Sleep(time.Microsecond * 10)
ch1 <- 2
time.Sleep(time.Microsecond * 10)
ch2 <- 1
}

func main() {
//获取电脑cpu数量
fmt.Printf("cpu num = %d\n", runtime.NumCPU())
runtime.GOMAXPROCS(runtime.NumCPU() - 1)
go Write()
go Read()
time.Sleep(time.Second * 2)
}

9.8 多协程间的同步

系统提供的sync.waitgroup

  • Add(delta int)添加协程记录
  • Done() 移除协程记录
  • Wait() 同步等待所有记录的协程全部结束
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var WG sync.WaitGroup
func TestSynWrite() {
for i := 1; i < 10; i++ {
WG.Add(1)
}
}

func TestSynRead() {
for i := 1; i < 10; i++ {
time.Sleep(time.Second * 1)
fmt.Printf("---> Done %d\n", i)
WG.Done()
}
}

func main() {
//获取电脑cpu数量
fmt.Printf("cpu num = %d\n", runtime.NumCPU())
runtime.GOMAXPROCS(runtime.NumCPU() - 1) //设置协程使用的最大核心数
TestSynWrite()
go TestSynRead()
WG.Wait()
}

10 并发任务

10.1 多任务

  • 仅执行一次

使用sync.Once确保函数只执行一次

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var singleInstance *Singleton
var once sync.Once

func GetSingletonObj() *Singleton {
once.Do(func() {
fmt.Println("Create Obj")
singleInstance = new(Singleton)
})
return singleInstance
}

func TestGetSingletonObj(t *testing.T) {
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
obj := GetSingletonObj()
fmt.Printf("%X\n", unsafe.Pointer(obj))
wg.Done()
}()
}
wg.Wait()
}

会发现以上对象obj只创建一次,打印出的地址都是相同的

  • 任一任务完成,则返回
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func runTask(id int) string {
time.Sleep(10 * time.Millisecond)
return fmt.Sprintf("The result is from %d", id)
}

func FirstResponse() string {
numOfRunner := 10
ch := make(chan string, numOfRunner)
for i := 0; i < numOfRunner; i++ {
go func(i int) {
ret := runTask(i)
ch <- ret
}(i)
}
return <-ch
}

func TestFirstResponse(t *testing.T) {
t.Log("Before:", runtime.NumGoroutine())
t.Log(FirstResponse())
time.Sleep(time.Second * 1)
t.Log("After:", runtime.NumGoroutine())

}

同时启动10个任务,只要任一任务完成,则channel中会有值,则立即返回,但为了防止其他Goroutine阻塞,则channel的大小和Goroutine的数量一致

  • 直到所有任务完成,才返回
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func AllResponse() string {
numOfRunner := 10
ch := make(chan string, numOfRunner)
for i := 0; i < numOfRunner; i++ {
go func(i int) {
ret := runTask(i)
ch <- ret
}(i)
}
finalRet := ""
for j := 0; j < numOfRunner; j++ {
finalRet += <-ch + "\n"
}
return finalRet
}

修改就是循环读取channel中所有数据

10.2 sync.Pool的使用

  • Sync.Pool总结
  1. 适合于通过复用,降低复杂对象的创建和GC代价
  2. 协程安全,会有锁的开销
  3. 生命周期受GC影响,不适合于做连接池等,需要自己管理生命周期的资源池化

11 测试

以上笔记使用的都是单元测试的框架,可以编写单元模块进行执行,练习。

11.1 内置单元测试框架

  1. Fail,Error:该测试失败,该测试继续,其他测试继续执行
  2. FailNow,Fatal:该测试失败,该测试中止,其他测试继续执行

以上是测试接口的差异,下边代码示例:

1
2
3
4
5
6
7
8
9
10
11
func TestErrorInCode(t *testing.T) {
fmt.Println("Start")
t.Error("Error")
fmt.Println("End")
}

func TestFailInCode(t *testing.T) {
fmt.Println("Start")
t.Fatal("Error")
fmt.Println("End")
}

以上TestFailInCode执行到Fatal后终止,并不会输出End

  • 代码覆盖率
1
go test -v -cover

使用以上命令行执行代码程序,可以展示出代码覆盖率。

  • 断言assert

断言模块
github.com/stretchr/testify/assert

使用断言时需要首先安装以上模块包

1
2
3
4
func TestAssert(t *testing.T){
assert.Equal(t, 1, 1)
}

以上测试两个参数相等,第二个参数是传入的值,第三个参数是期望值。

11.2 Benchmark

Benchmark可以测试代码块的执行时间,以此测试代码性能。
使用方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
func BenchmarkConcatStringByAdd(b *testing.B) {

elems := []string{"1", "2", "3", "4", "5"}
b.ResetTimer()
for i := 0; i < b.N; i++ {
ret := ""
for _, elem := range elems {
ret += elem
}
}
b.StopTimer()
}

输出

可以看到限制代码块执行时间120ns/op

11.3 BDD in go

项目网站:
https://github.com/smartystreets/goconvey

安装
go get -u github.com/smartystreets/goconvey/convey

启动WEB UI
$GOPATH/bin/goconvey

12 反射编程

13 常见任务

13.1 json工具

  1. 内置json解析
1
2
3
4
5
6
7
import "encoding/json"

//把json解析到指定结构
func Unmarshal(data []byte, v interface{}) error

//把指定结构转换成json
func Marshal(v interface{}) ([]byte, error)
  1. easyjson模块

参考:easyjson的使用
easyjson相对于内置json而言,解析更快,是通过代码生成,而非发射机制

需要首先安装easyjson模块

1
go get -u github.com/mailru/easyjson/...

使用命令生成代码:
easyjson -all <结构定义>.go