Golang知识补充

记录一些golang学习过程中没有详细关注到,遗漏的知识点。

基础知识

变量

内建变量

bool:内存占用1个字节

string:用的比较多,内存占用2个字节,string数据结构是一个指针和int类型长度,指针是uintptr类型的8位int,长度也是int类型,一共16位,2个字节

(u)int,(u)int8,(u)int16,(u)int32,(u)int64 整形,占用内存就是后面指定的位数

uintptr 指针,int类型,

byte:uint8的别名,占1个字节

rune:字符型(类似char,是int32的别名 ),占4个字节

Float32,float64 浮点,占用内存是后面指定的位数

complex64,complex128 复数,占用内存是后面指定的位数

rune

golang支持 Unicode的字符集,通过 UTF-8规则存储,一个英文占1字节,中文占3字节。字符称为runeint32的别称

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
s := "hello世界!"
fmt.Println(len(s)) // 获得字节长度 12,汉字6个字符
for i, j := range []byte(s) {
fmt.Printf("(%d:%x)", i, j) // (0:68)(1:65)(2:6c)(3:6c)(4:6f)(5:e4)(6:b8)(7:96)(8:e7)(9:95)(10:8c)(11:21) // unicode编码,直接使用unicode编码转义,会出现错乱字符
}
fmt.Println()
for i, j := range s {
fmt.Printf("(%d:%x)", i, j) // (0:68)(1:65)(2:6c)(3:6c)(4:6f)(5:4e16)(8:754c)(11:21) // UTF8编码,一个汉字占三个字节,因此无法获取索引为 6 7 9 10
}
fmt.Println()
for i, j := range []rune(s) {
fmt.Printf("(%d:%c)", i, j) // (0:h)(1:e)(2:l)(3:l)(4:o)(5:世)(6:界)(7:!) // 类型转换,每一个rune占4个字节,重新分配内存
}
fmt.Println()
fmt.Println(utf8.RuneCountInString(s)) // 8 获得字符数
编码与存储

例如一个字符 “中”

1
2
3
4
5
    x := "中"
// Unicode编码
fmt.Printf("中 Unicode: %x\n", []rune(x)[0]) // 中 Unicode: 4e2d
// UTF-8存储
fmt.Printf("中 UTF-8: %x\n", x) // 中 UTF-8: e4b8ad

匿名变量

1
_,a := 10,20 // 匿名变量不占用内存空间,不会分配内存。匿名变量和匿名变量之间不会因为多次声明而无法使用

格式化输出输入

格式 含义
%% 返回%,一个%字面量
%b 整数,返回一个整数的二进制
%c 整数,返回字符集对应整数序号的字符
%d 整数,返回一个整数的十进制
%f 浮点数或者复数,返回一个浮点数或者复数
%o 整数,返回一个整数的八进制
%p 指针变量,返回一个十六进制的地址
%q 整形,返回单引号围绕的字符字面值,由Go语法安全地转义,
%s 字符串或者字节切片,返回一个字符串
%t bool,返回true或者false
%T 变量,返回这个变量的类型
%v 变量,返回这个变量的值,配合%+v %#v可以详细打印字段名
%x 整形或者[]byte,返回以a-f小写表示的十六进制
%X 整形或者[]byte,返回以A-F大写表示的十六进制

强制类型转换

1
2
3
a, b := 3, 4
var c int
c = int(math.Sqrt(float64(a*a + b*b))) // float 转int,如果浮点计算出4.999转int可能答案是4

常量

常量的类型只能是整形、字符串、bool,常量定义之后,可以不使用。

常量分无类型常量和有类型常量,无类型常量在使用的时候,可作为各种类型使用。

1
2
3
4
5
const a = 1
const b int64 = a
var c float64 = a
fmt.Println(b)
fmt.Println(c)

枚举类型

iota:当iota第一次在const中出现的时候,是0,在const块后续中,iota自增;当后续常量复制没有指定,则默认与上面一致。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const (
a = iota // iota = 0
_ // _ = iota // iota = 1
b // b = iota // iota = 2
c
d
)
fmt.Println(a, b, c, d)
// b, kb, mb, gb, tb, pb
const (
byte = 1 << (10 * iota)
kb
mb
bg
tb
pb
)
fmt.Println(byte, kb, mb, bg, tb, pb)

条件

1
2
3
4
if a, err := newerr(); err != nil { // if条件中赋值,作用域是这个条件语句中
fmt.Println(err)
}
fmt.Println(a) // 出错

参数传递

Golang中的传递都是值传递,没有引用传递。即使是传递的指针变量,也是拷贝一个指针变量,只是这个指针变量指向的内存地址是同一个,所以函数内部修改会作用到相同的内存地址。

值类型:intfloatboolstring、数组、结构体

引用类型:指针、slice切片、mapchannelinterface、函数

数组

数组关注的比较少,相比切片而言。

数组特点是,数组的长度是固定的不可改变,数组的个数不可以是变量,可以是常量。数组中每个元素的地址是连续的。[5]int[10]int是不同类型,因此这两个数组不能相互比较;

数组是值传递,被函数调用的时候,会拷贝数组到函数中;

1
2
3
4
5
6
7
8
9
10
func newerr(a [2]int) { // 值传递,传入的是拷贝后的一个数组
a[0] = 100
return
}
// 当然,如果传入的是一个指针,那么函数中的改动,会影响变量本身。go中一般不使用数组。
func main() {
a := [2]int{1, 2}
newerr(a)
fmt.Println(a) // [1 2]
}

数组的地址为首元素地址

1
2
3
a := [3]int{1, 2, 3}
fmt.Printf("%p\n", &a) // 0x1400012e000
fmt.Printf("%p\n", &a[0]) // 0x1400012e000

切片

本身是指针类型变量,由一个指向底层切片的指针、长度、容量组成。

切片的类型定义,在runtime包的slice.go

1
2
3
4
5
type slice struct {
array unsafe.Pointer
len int
cap int
}

可以由数组的位置值定义,左开右闭

1
2
a := []int{0, 1, 2, 3, 4, 5, 6}
fmt.Println(a[2:7]) // 这里使用7,代表到6,其实7是超过了长度的,a[7]会panic //panic: runtime error: index out of range [7] with length 7

切片对数组的引用

1
2
3
4
5
6
7
8
9
10
11
12
13
func newerr(a []int) {
a[0] = 100
return
}

func main() {
a := [7]int{0, 1, 2, 3, 4, 5, 6}
b := a[3:5]
newerr(b)
c := a[:]
newerr(c)
fmt.Println(a) // [100 1 2 100 4 5 6]
}

切片对数组的引用

1
2
3
4
5
6
7
a := [...]int{0, 1, 2, 3, 4, 5, 6, 7}
b := a[2:6]
c := b[3:5]
fmt.Println(c) // [5 6] 通过下标可以获取到底层灰色部分的数组
fmt.Println(c[2]) // panic: runtime error: index out of range [2] with length 2 直接通过下标访问不到底层数组
fmt.Println(len(c)) // 2
fmt.Println(cap(c)) // 3

image-20220623004223727

切片和数组的关系

image-20220623004445749

slice可以向后扩展,不能向前扩展。

切片对底层数组的引用

1
2
3
4
s1 := [3]int{1}
s2 := s1[1:]
fmt.Printf("%p\n%p\n", &s1, &s1[0]) // 0x1400012e000 0x1400012e000
fmt.Printf("%p\n%p\n%p\n", s2, &s2, &s2[0]) // 0x1400012e008 0x1400011e018 0x1400012e008

通过索引访问切片中的内容需要注意,范围可以访问到cap,但是索引只能访问len

1
2
3
4
5
func main() {
s := make([]int,0,10)
fmt.Println(s[0]) // panic: runtime error: index out of range [0] with length 0
fmt.Println(s[5:8]) // [0,0,0]
}

append

append只能接受切片,而且是值传递(拷贝指针),需要有变量接收返回值。

当切片没有扩容,则指向的内存是同一块,因此改动切片中的内容会同步修改原数组,当切片发生扩容,则需要开辟新的内存空间用于存储新的切片,此时改动切片的内容,原数组不会变化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
a := [...]int{0, 1, 2, 3, 4, 5, 6, 7}
b := a[2:6] // [2,3,4,5]
c := b[3:5] // [5,6]
fmt.Println(c) // [5,6]
fmt.Println(len(c)) // 2
fmt.Println(cap(c)) // 3 这里的3是因为底层的数组
d := append(c, 10) // [5,6,10]
// 从这里开始,d到d1,由于cap发生变化,底层的数组就不是同一个,d1底层的数组会重新拷贝一个出来
d1 := append(d, 10) // [5,6,10,10]
d[0] = 300 // 此处改变的是a数组 [300 6 10]
d1[0] = 200 // 数组已经发生拷贝,原先的a数组不会发生变化,新的数组 [200,6,10,10]
d2 := append(d1, 10) // [200,6,10,10,10] // 由于值传递,必须接受append的返回值
d3 := append(d2, 10) // [200,6,10,10,10,10]
d4 := append(d3, 10) // [200,6,10,10,10,10,10]
fmt.Println(d, d1, d2, d3) //
fmt.Println(len(d4)) // 7 ,有元素的个数
fmt.Println(cap(d4)) // 12 ,从6到12,翻倍扩容cap
fmt.Println(a) // [0 1 2 3 4 300 6 10] // 原先的a是300,后续改变的200改的不是这个a // 另外,如果这里没有使用a,a会被gc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func ChangeSlice(slice []int) { // 引用传递,函数内部改变,引起变量本身改变
slice[0] = 100
}

func AppendSlice(slice []int) { // 引用传递,但是函数内部赋值不会改变指向的底层数组
fmt.Printf("slice:%v, len: %d,cap: %d\n", slice, len(slice), cap(slice))
slice = append(slice, 100) // append会扩容切片,并且是值传递,首先拷贝slice的值,扩容之后有一个新的值,接受到的slice是一个新的slice,新的slice被赋值不会改变入参
fmt.Printf("newslice:%v, len: %d,cap: %d\n", slice, len(slice), cap(slice))
}

func AppendSlicePtr(slice *[]int) {
fmt.Printf("slice:%v, len: %d,cap: %d\n", *slice, len(*slice), cap(*slice))
*slice = append(*slice, 100) // append扩容切片,拷贝的是地址,赋值的也是地址,因此底层数据会改变
fmt.Printf("newslice:%v, len: %d,cap: %d\n", *slice, len(*slice), cap(*slice))
}

函数内部append需要注意,在函数内部进行append的时候,修改了slice的len,但是这个修改不影响函数外部,因此外部的slice的len还是1

1
2
3
4
5
6
7
8
9
10
11
12
13
func changeSlice(arr []int) {
arr = append(arr, 666) // 函数内容发生append,但是没有扩容,但是len变成2
}

func main() {
arr := make([]int, 0, 10)
arr = append(arr, 123)
changeSlice(arr) // 函数是copy传递,函数内部len变成2,不影响函数外部,因此只会打印1个
fmt.Println(arr) // output ??
}

// Reslut:
// [123]

切片声明和初始化

1
2
3
4
5
func main() {
s1 := make([]int, 1) // 初始化,完成内存分配,0x1400018c008
var s1 []int // 只有声明,没有分配内存,0x0
fmt.Printf("%p\n", s1)
}

切片扩容时先判断容量是否满足,不满足则容量翻倍(容量增加算法不是一直翻倍,golang版本更迭,append的源码有变更 ),然后将数据拷贝到新的切片中。

1
2
3
4
5
s1 := make([]int, 0)
for i := 0; i < 2048; i++ {
s1 = append(s1, i)
fmt.Printf("%p,%d %d\n", s1, len(s1), cap(s1))
}
1
2
3
4
5
6
7
8
9
10
11
12
13
0x1400012c008,1 1
0x1400012c020,2 2
0x14000130020,3 4
0x14000130020,4 4
0x14000132000,5 8
0x1400013e000,512 512
0x14000142000,513 848 // 512 -> 848 1.65倍
0x14000142000,848 848
0x14000150000,849 1280 // 848 -> 1280 1.5倍
0x14000150000,1280 1280
0x1400015e000,1281 1792 // 1280 -> 1792 1.4倍
0x1400015e000,1792 1792
0x14000172000,1793 2560 // 1792 -> 2560 1.4倍

copy

copy可以实现深层拷贝,拷贝底层数组,然后赋值。copy的过程不发生内存拷贝,也就是不会改变数组的len和cap

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
s1 := [...]int{1, 2, 3, 4, 5, 6, 7, 8, 9}
s2 := s1[4:] //5 6 7 8 9 size 5 cap 5
s3 := s1[6:] //7 8 9 size 3 cap 3
fmt.Println(s2, s3)
copy(s2, s3) // 1 2 3 4 7 8 9 8 9
//copy(s3, s2)
fmt.Println(s2, s3) // 7 8 9 8 9 // 9 8 9ca

s1 := [...]int{1, 2, 3, 4, 5, 6, 7, 8, 9}
s2 := s1[4:] //5 6 7 8 9 size 5 cap 5
s3 := s1[6:] //7 8 9 size 3 cap ®ca3
fmt.Println(s2, s3)
//copy(s2, s3)
copy(s3, s2) // 1 2 3 4 5 6 5 6 7
fmt.Println(s2, s3) // 5 6 5 6 7 // 5 6 7

map

源码在runtime/map.go中

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
42
43
44
45
46
// A header for a Go map.
type hmap struct {
// Note: the format of the hmap is also encoded in cmd/compile/internal/reflectdata/reflect.go.
// Make sure this stays in sync with the compiler's definition.
count int // # live cells == size of map. Must be first (used by len() builtin)
flags uint8
B uint8 // log_2 of # of buckets (can hold up to loadFactor * 2^B items)
noverflow uint16 // approximate number of overflow buckets; see incrnoverflow for details
hash0 uint32 // hash seed

buckets unsafe.Pointer // array of 2^B Buckets. may be nil if count==0.
oldbuckets unsafe.Pointer // previous bucket array of half the size, non-nil only when growing
nevacuate uintptr // progress counter for evacuation (buckets less than this have been evacuated)

extra *mapextra // optional fields
}

// mapextra holds fields that are not present on all maps.
type mapextra struct {
// If both key and elem do not contain pointers and are inline, then we mark bucket
// type as containing no pointers. This avoids scanning such maps.
// However, bmap.overflow is a pointer. In order to keep overflow buckets
// alive, we store pointers to all overflow buckets in hmap.extra.overflow and hmap.extra.oldoverflow.
// overflow and oldoverflow are only used if key and elem do not contain pointers.
// overflow contains overflow buckets for hmap.buckets.
// oldoverflow contains overflow buckets for hmap.oldbuckets.
// The indirection allows to store a pointer to the slice in hiter.
overflow *[]*bmap
oldoverflow *[]*bmap

// nextOverflow holds a pointer to a free overflow bucket.
nextOverflow *bmap
}

// A bucket for a Go map.
type bmap struct {
// tophash generally contains the top byte of the hash value
// for each key in this bucket. If tophash[0] < minTopHash,
// tophash[0] is a bucket evacuation state instead.
tophash [bucketCnt]uint8
// Followed by bucketCnt keys and then bucketCnt elems.
// NOTE: packing all the keys together and then all the elems together makes the
// code a bit more complicated than alternating key/elem/key/elem/... but it allows
// us to eliminate padding which would be needed for, e.g., map[int64]int8.
// Followed by an overflow pointer.
}

kv存储,无序,如果k不在,返回的value是对应类型的初始值。

两个map不能比较是否相等;map的key或者value都不能取地址。

map使用哈希表,必须可以比较相等或者不想等。除了slice map function之外的内建类型都可以做key。(slicemap都是引用类型,无法相互比较)(strcut可以比较的话,也可以做key,因此作为keystruct内不包含上面三个类型即可)。

另:float类型作为key,编译器不会报错,但是由于精确度问题,使用浮点型作为map的key,可能会出现数据访问不准确。

声明和初始化:

1
2
3
4
5
6
7
8
9
10
11
12
13
m := map[struct {
A int
B string
C IA // key可以是一个带方法的interface
//D func() int // key 不可以是函数
E interface{} // key也可以是一个不带方法的空interface
//F []int // key不可以是切片
}]int{}
fmt.Printf("%p\n", m) // 0x14000106180
fmt.Printf("m == nil %t\n", m == nil) // false
var m1 map[int]int
fmt.Printf("%p\n", m1) // 0x0
fmt.Printf("m1 == nil %t\n", m1 == nil) // true

指针

指针包含两个部分,类型和地址。指针的默认值是nil 。

nil和nil不能判断是否相等,编译器不会报错,运行和编译的时候会报错

1
2
fmt.Println(nil == nil)
// invalid operation: nil == nil (operator == not defined on untyped nil)

不同类型的 nil 指针是一样的,都是0x0

1
2
3
var a *int
var b []int
fmt.Printf("%p %p\n", a, b) // 0x0 0x0

不同类型的 nil指针,不能判断是否相等

1
2
3
var a *int
var b []int
fmt.Println(a == b) // invalid operation: a == b (mismatched types *int and []int)

即使是同类型指针,如果类型不能判断是否相等,则两个 nil 指针也不能判断是否相等

1
2
3
var a *string
var c *string
fmt.Println(a == c) // true
1
2
3
var a []int
var c []int
fmt.Println(a == c) // invalid operation: a == c (slice can only be compared to nil)

nil 是 map slice pointer channel func interface的零值,不同类型的零值占用的内存不一致。

各种数据类型初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var a int
fmt.Printf("%p %v\n", &a, a) // 0x1400012c008 0
var b string
fmt.Printf("%p %v\n", &b, b) // 0x14000110210
var c [1]int
fmt.Printf("%p %v\n", &c, c) // 0x1400012c020 [0]
var d []int
fmt.Printf("%p %v %p\n", &d, d, d) // 0x1400011e018 [] 0x0
var e map[int]int
fmt.Printf("%p %v %p\n", &e, e, e) // 0x14000126020 map[] 0x0
var f chan int
fmt.Printf("%p %v %p\n", &f, f, f) // 0x14000126028 <nil> 0x0
var g func()
fmt.Printf("%p %v %p\n", &g, f, g) // 0x14000126030 <nil> 0x0

指针类型和值类型

指针类型和值类型有时候可以相互转换,但是需要注意。值类型一直都可以转换为引用类型,值是零值在编译器下可以寻址。但是引用类型只有在可以寻址的情况下才可以转换成值类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type User struct {
name string
Addresses []*Address
}

type Address struct {
s string
}

func main() {
u := User{
name: "test",
Addresses: make([]*Address,10), // 即使这里初始化
}
u.Addresses[0].s = "local" // 访问u.Addresses[0]是一个nil,无法访问到u.Addresses[0].s,是一个空指针
}

需要改成

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

type User struct {
name string
Addresses []Address
}

type Address struct {
s string
}

func main() {
u := User{
name: "test",
Addresses: make([]Address,10),
}
u.Addresses[0].s = "local"
}

或者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type User struct {
name string
Addresses []*Address
}

type Address struct {
s string
}

func main() {
u := User{
name: "test",
Addresses: make([]*Address,0,10),
}
address := Address{s: "local"}
u.Addresses = append(u.Addresses,&address)
}

new和make

new:为一个类型T分配空间,并且初始化为T的零值,返回的是新值的地址,传递给new函数的是一个类型,而不是值。

make:返回初始化之后T类型的值,而不是零值,也不是地址。只能用于slice map channel

例如通过new初始化一个slice,返回的是指向这个slice的指针,而slice本身是nil。

1
2
3
n1 := new([]int)
fmt.Printf("%v %p\n", *n1, *n1) // [] 0x0
fmt.Println(*n1 == nil) // true
1
2
3
m1 := make([]int, 0)
fmt.Printf("%v %p\n", m1, m1) // [] 0x1023d2fe8
fmt.Println(m1 == nil) // false

注意:作为函数的返回值,返回值定义了类型,并没有分配地址

1
2
3
4
func returnSlice() (s []int) {
fmt.Printf("%v %p\n", s, s) // [] 0x0
return
}

类型定义 type

类型定义有以下几种:

  • type 类型名 类型
  • type 类型名 = 类型名
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type S struct {
a int
}

type T1 = S // 定义别名,别名和原类型一致,别名可以调用原类型方法
type T2 S // 重新定义一个新的类型

func main() {
var s1 = S{a: 1}
var s2 = T1{a: 1}
var s3 = T2{a: 2}
fmt.Println(s1 == s2)
fmt.Println(s1 == s3) // 无效运算: s1 == s3(类型 S 和 T2 不匹配)
fmt.Println(s2 == s3) // 无效运算: s2 == s3(类型 T1 和 T2 不匹配)
}

结构体方法

结构体方法需要将结构体变量作为参数,有两种方式,一种是值传递,一种是指针传递。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type User struct {
name string
age int
}

func (u *User) changeAge(age int) {
fmt.Printf("ptr: %p\n", u)
u.age = age
return
}

func (u User) changeName(name string) {
fmt.Printf("value: %p\n", &u)
u.name = name
return
}

在声明变量的时候,无论变量是值类型还是指针类型,在调用方法的时候,会按照方法定义时候做出改变。

  • 值类型,在值传递,会拷贝一份变量,传递的是拷贝后的值。
  • 值类型,在引用传递,会使用变量的地址,传递的是变量的地址。
  • 指针类型,在值传递,会拷贝一份指针类型指向的变量,传递的是拷贝后的变量。
  • 指针类型,在引用传递,会使用指针类型的值,传递的是指针类型的值。

需要注意的是,如果如果是声明一个类型变量之后隐式调用,即无法准确获取到变量的指针,此时无法使用引用传递

1
2
3
4
5
6
7
8
9
User{
name: "",
age: 0,
}.changeName("abc")

User{
name: "",
age: 1,
}.changeAge(18) // cannot call pointer method changeAge on User

另外,虽然值类型也可以使用引用传递,但是在接口定义的时候,会严格验证接口内方法实现,接口内方法定义传参是值类型,则实现函数必须是传值,接口内方法定义传参是引用类型,则实现函数必须是传指针。

另外:

  • 结构体只能判断== != ,不能比较大小

  • 判断==、!=的前提是两个结构体是同一个类型,不同的类型无法判断,即使结构体里面的成员一样也无法判断

    1
    2
    3
    4
    5
    6
    7
    8
    9
    u := User{
    name: "",
    age: 0,
    }
    u1 := User1{
    name: "",
    age: 0,
    }
    fmt.Println(u == u1) // invalid operation: u == u1 (mismatched types User and User1)

面向对象

Golang仅支持封装,不支持继承和多态。通过结构体的方法实现封装。

构造函数(工厂方法)

1
2
3
4
5
6
7
type Node struct {
value int
}

func NewNode(va int) *Node {
return &Node{value: va}
}

可以看到,构造函数内部创建一个局部变量,返回这个局部变量的指针。当这个局部变量会被其他函数引用,则会被编译器分配到堆上。

值传递,传递指针或者拷贝的变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func (n Node) setValue(va int) { // golang 都是值传递,传入的是拷贝的node
n.value = va
}

func (n *Node) setValueNew(va int) { // 拷贝的是node的指针,改动指针指向的地址
n.value = va
}

func main() {
node := NewNode(1)
node.setValue(10)
node.print() // 1
node.setValueNew(100) // 不需要使用&node来调用setValueNew方法,golang自动传入指针
node.print() // 100
}

引用

1
2
3
4
5
node1 := Node{2}    // node1是指针类型
node1.setValue(20) // 调用时候,会获取到node1的值,拷贝值,将拷贝后的值作为参数
node1.print() // 2
node1.setValueNew(200)
node1.print() // 200

nil指针也可以调用方法

1
2
var node2 *Node
node2.print() // 这里会将node2的值拷贝进去,但是由于node2是nil指针,无法访问对象对应的值,因此会报错:panic: runtime error: invalid memory address or nil pointer dereference

值接受者和指针接受者:

  • 改变内容必须使用指针接受者
  • 结构过大也考虑指针接受者
  • 已有指针接受者,最好都是指针接受者

扩展其他的包的方法,定义别名和使用组合、内嵌

1
2
3
4
5
6
7
8
9
// 使用组合
type myNode struct {
node *Node
}

func (m *myNode) Print() { // 重写方法
fmt.Println()
m.node.print() // 调用内部对象的方法
}

内嵌

1
2
3
4
5
6
7
8
type myNode struct {
*Node
}

func (m *myNode) Print() { // 同名方法会覆盖子类的方法
fmt.Println()
m.Node.Print() // 调用Node的Print方法
}

依赖管理

GOPATH:最开始的方案,将依赖放到GOPATH中,会将所有第三方的库都放在src下。使用依赖的时候,会往GOROOTGOPATHsrc中查询。

劣势:所有的依赖全部放在src中,src会非常庞杂,而且无法解决不同的包依赖不同的版本问题。

VENDER:将本服务的依赖包放在本服务目录的vendor下。。使用依赖的时候,会往VENDER目录、GOROOTGOPATHsrc中查询。

劣势:出现大量第三方依赖管理工具,管理过程也是非常冗杂,而且代码需要附带vendervender中可能还有vendor

GO MODULE:在开启GO MODULE时,使用go get -u xxx即可将这个包的某个版本加入到GO MODULE中。默认使用最新版,可通过go get -u xxx@version 指定版本。由go统一管理,

go.mod:记录所有依赖的包,以及对应版本

go.sum:记录所有依赖包以及这些包的依赖

使用方法:

  1. 创建项目的时候创建go mod
  2. go mod init xxx && go build ./...

在当前项目中,使用go buildgo install

1
2
3
go build main.go 编译main.go文件,编译成功将编译的结果放到当前目录
go build ./... 检查所有的代码的main函数是否可以正常编译
go install ./... 将所有代码中包含main函数的代码编译到go path的bin目录下

接口

抽象的概念

1
2
3
4
5
6
7
8
type Retriver interface {
Get(string) string
}

type Retrive struct {
}

func (*Retrive) Get(url string) string {}

类型断言 type assertion

1
2
3
4
5
6
7
8
testretrive := r.(test.TestRetrive)
fmt.Println(testretrive.Content)

if testretrive, ok := r.(retrive.Retrive); ok {
fmt.Println(testretrive.TimeOut)
} else {
fmt.Println("retrive.Retrive is not a testretrive")
}

所以接口变量里面,有两个内容,一个是实现者的类型,一个是实现者的值。

接口变量自带指针;接口变量采用值传递,几乎不需要使用接口的指针;指针接受者实现只能以指针方式使用,值接受者都可以。

1
2
3
var r1 retrive.Retriver
r1 = &retrive.Retrive{} // 由于Retrive只实现了指针的Get方法,因此此处只能是指针
r1 = retrive.Retrive{} // 无法将 'retrive.Retrive{ UserAgent: "", TimeOut: 0, }' (类型 retrive.Retrive) 用作类型 retrive.Retriver 类型未实现 'retrive.Retriver',因为 'Get' 方法有指针接收器
1
2
3
r2 = test.TestRetrive{Content: "test"}      
// r2 = &test.TestRetrive{Content: "test"} // TestRetrive 是值实现Get方法,因此无论指针接受者还是值接受者都可以
fmt.Println(r2.Get("test"))

接口组合

1
2
3
4
5
6
7
8
9
10
11
// ReadCloser is the interface that groups the basic Read and Close methods.
type ReadCloser interface {
Reader // 将多个接口组合成一个接口
Closer
}

// WriteCloser is the interface that groups the basic Write and Close methods.
type WriteCloser interface {
Writer
Closer
}

内部接口

三个重要的内部接口

Stringer

1
2
3
4
5
6
type Stringer interface {
String() string
}

fmt.Println(r1) // this is String: r1 retrive 实现String方法,打印时则打印内容
fmt.Println(r2) // &{test} 没有实现String方法,打印本身

ReaderWriter,抽象文件,不仅可以读写文件,还可以读字符串一样读文件或者一个网络io

1
reader := strings.NewReader(str)

闭包

函数和外部环境(例如变量)

特点1:可接受值传递或者引用传递。会一直保留外部变量的值。

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
func foo1(x *int) func() {
return func() {
*x = *x + 1
fmt.Printf("foo1 val = %d\n", *x)
}
}

func foo2(x int) func() {
return func() {
x = x + 1
fmt.Printf("foo2 val = %d\n", x)
}
}

x := 133
f1 := foo1(&x)
f2 := foo2(x)
f1() // x 134 print 134 指针拷贝
f2() // x 134 print 134 值拷贝,拷贝的是当时的133
f1() // x 135 print 135 指针拷贝,传入的是134,加1,成为135
f2() // x 135 print 135 闭包,值拷贝,拷贝的是上一个f2()的134,会记录外部环境的值
// Q1第二组
x = 233
f1() // x = 234 print 234
f2() // 136 print 136 拷贝的是上一个f2()的135
f1() // x = 235 print 235
f2() // 137 print 137 拷贝的是上一个f2()的136
// Q1第三组 x = 235
foo1(&x)() // 236 print 236
foo2(x)() // 237 print 237 将236当前x拷贝进去
// x = 236
foo1(&x)() // 237 print 237
foo2(x)() // 237 print 238
foo2(x)() // 238 print 239

特点2:延迟调用,在函数被调用的时候,才会去获取外部环境的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func foo7(x int) []func() {
var fs []func()
values := []int{1, 2, 3, 5}
for _, val := range values {
fs = append(fs, func() {
fmt.Printf("foo7 val = %d\n", x+val)
})
}
return fs
}

fs := foo7(3)
for _, f := range fs {
f() // 被调用的时候去获取x和val,其中x是传入的3,val是values中最后一个值5,最终都是8
}

defer

defers后的函数压栈,先进后出。在函数中,defer是在return之后执行。

一般用于文件打开之后defer file.Close()

1
2
3
4
5
file, err := os.Create("./test.txt")
defer file.Close()

reader := bufio.NewWriter(file)
defer reader.Flush()

参数在defer语句时计算。出现defer,计算参数的值,存储在defer语句中。

1
2
3
4
5
6
7
8
9
var a int
a = 1
b := &a
*b = 2
fmt.Println("a:", a) // a: 2
defer fmt.Println("defer a:", a) // defer a: 2
defer fmt.Println("defer *b:", *b) // defer *b: 2
*b = 3
fmt.Println("last a:", a) // last a: 3
1
2
3
4
5
6
7
8
9
10
11
func double(i int) int {
return i * 2
}

func x(i int) (r int) {
defer func() {
fmt.Println("defer: ", r) // 16 defer在return之后执行
}()
fmt.Println(r) // 0
return double(i)
}

错误处理

封装函数,封装过程中处理报错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func wraperError(handle handler) func(writer http.ResponseWriter, request *http.Request) {
return func(writer http.ResponseWriter, request *http.Request) {
err := handle(writer, request)
switch {
case os.IsNotExist(err):
log.Println(err)
http.Error(writer, http.StatusText(http.StatusNotFound), http.StatusNotFound)
}
}
}

func fibo(writer http.ResponseWriter, request *http.Request) error {}

http.HandleFunc("/text/", wraperError(fibo))

使用type assetion类型断言,对错误进行判断

1
2
3
4
5
6
7
8
if !strings.Contains(request.URL.Path, prefix) {
return userError("path must contains " + prefix)
}

if userErr, ok := err.(userError); ok {
http.Error(writer, userErr.Message(), http.StatusInternalServerError)
return
}

测试

测试分为testing.Ttesting.B,前者重点在测试功能和代码覆盖率,后者重点在测试性能。

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 TestBest(t *testing.T) {
tests := []struct { // 表格驱动测试
s string
num int
}{
{"abc", 3}, // 测试用例通过数组话
{"a", 1},
{" ", 1},
{"abcabcabcd", 4},
{"aabbccddef", 3},
{"abcabcbb", 3},
{"bbbbb", 1},
{"pwwkew", 3},
{"abba", 2},

// chinese test case
{"你好呀世界", 5},
{"你从哪里来,我的朋友,好像一只蝴蝶~", 12},
}
for _, tt := range tests {
if res := findlongeststr(tt.s); res != tt.num {
t.Errorf("findlongeststr with str %s get %d inpect %d", tt.s, res, tt.num) // 测试结果通过t给出
}
}
}

➜ findLongestStr go test // 命令行
PASS
ok gostudy/findLongestStr 0.093s

➜ findLongestStr go test -coverprofile=c.out // 将覆盖报告生成一个文件
PASS
coverage: 100.0% of statements

➜ findLongestStr go tool cover -html=c.out // html的方式展示

1
2
3
4
5
6
7
8
9
10
11
12
func BenchmarkBest(b *testing.B) {
for i := 0; i < b.N; i++ {
s, num := "你从哪里来,我的朋友,好像一只蝴蝶~", 12
if res := findlongeststr(s); res != num {
b.Errorf("findlongeststr with str %s get %d inpect %d", s, res, num)
}
}
}

➜ findLongestStr go test -bench . // 命令行

BenchmarkBest-10 1441500 833.6 ns/op // 测试144万次,平均一次833ns

pprof 性能优化

测试过程采集CPU性能指标

1
➜  findLongestStr go test -bench . -cpuprofile cpu.out

查看

1
2
➜  findLongestStr go tool pprof cpu.out
(pprof) web 通过svg的形式打开页面

image-20220701011654889

通过svg图可以看到耗费时间的步骤在什么地方,可以针对性优化。例如上图耗时在maphash、插入、扩容,可以通过空间换时间,换成使用一个很大的数组,将内容填充到数组的index上。

http测试

使用假的Request Response做测试

1
2
3
4
5
6
f := wraperError(tt.errorHandler) // 测试函数
writer := httptest.NewRecorder() // 假的response
request := httptest.NewRequest(http.MethodGet, "https://www.baidu.com", nil) // 假的request
f(writer, request)
bytes, _ := ioutil.ReadAll(writer.Body)
data := string(bytes)

起服务器

1
2
3
4
f := wraperError(tt.errorHandler)
server := httptest.NewServer(http.HandlerFunc(f))
resp, _ := http.Get(server.URL) // 使用http服务测试
bytes, _ := ioutil.ReadAll(resp.Body)

文档

go doc查看某个包的文档

1
2
3
4
5
6
7
8
➜  ptrinterface go doc fmt.Println  // 获取某一个包中某个方法的文档
package fmt // import "fmt"

func Println(a ...any) (n int, err error)
Println formats using the default formats for its operands and writes to
standard output. Spaces are always added between operands and a newline is
appended. It returns the number of bytes written and any write error
encountered.

也可以通过godoc打开说明文档

1
➜  findLongestStr godoc -http localhost:6060

image-20220701111024462

example : 实例代码,并且可以检查输出是否正确

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func ExampleQueue_Pop() {
q := Queue1{}
q.Push(1)
q.Push(2)
q.Push(3)
q.Push(4)
fmt.Println(q.Pop())
fmt.Println(q.Pop())
fmt.Println(q.Pop())
fmt.Println(q.Pop())

// OutPut:
// 1
// 2
// 3
// 4
}

image-20220701141652787

内存分配

golang中的内存分配有两种方式,一种是分配在栈上,一种是分配在堆上。

栈的数据结构是先进后出,分配和回收速度快,在golang中会将一些局部变量分配在栈上,例如

1
2
3
4
5
func calc(a, b int) int {
c := a + b
x := c * 10
return x
}

在调用函数calc时,会将变量c和x分配在栈上,函数调用完成,变量c和x的内存会被自动回收。

堆是无序的,堆分配内存按照变量大小分配,容易造成内存碎片,相比栈分配,堆适合大小不可预知的内存分配,因此分配和回收速度较慢。

逃逸分析

通过编译器分析代码的特征和代码的生命周期,决定应该使用堆还是使用栈进行内存分配。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func calc(a, b int) int {
c := a + b
x := c * 10
return x
}

func cacl1(a int) int {
var b int // b 是整形,通过返回值"逃出"了cacl1函数,但是是b的值的拷贝作为返回值,即使b被回收,也不影响在main()中使用返回值
b = a
return b
}

func none() {

}

func main() {
var a int
none()
fmt.Println(calc(1, a))
fmt.Println(a, cacl1(10))
}
1
go run -gcflags "-m -l" main.go 
  • -gcflags 编译参数,-m 进行内存分配分析 -l 表示避免程序内联,避免程序优化
1
2
3
4
5
6
./main.go:24:13: ... argument does not escape
./main.go:24:18: calc(1, a) escapes to heap // calc(1,a)逃逸到堆上,由于函数有返回值,被fmt.Println使用后还是会在main()函数中继续存在。
./main.go:25:13: ... argument does not escape
./main.go:25:13: a escapes to heap // a 逃逸到堆上
./main.go:25:22: cacl1(10) escapes to heap

除了通过逃逸分析,还可以通过取地址

1
2
3
4
5
6
7
8
9
10
11
type Data struct {
}

func NewData() *Data {
var a Data // moved to heap: a
return &a
}

func main() {
fmt.Println(NewData())
}
1
2
3
go run -gcflags "-m -l" main.go
./main.go:25:6: moved to heap: a // 变量a在函数外部也会被使用,移动到堆上
./main.go:30:13: ... argument does not escape

编译器觉得变量应该分配在堆和栈上的原则是:

  • 变量是否被取地址
  • 变量是否发生逃逸

并发编程

协程:轻量级“线程”;非抢占式多任务处理,由协程主动交出控制权;编译器、解释器、虚拟机层面的多任务;多个协程可以在一个或多个线程上运行。

  • 非抢占式:线程是抢占式,当CPU中断切换线程时,会保存当前现成的上下文。协程非抢占,当一个协程执行过程中,如果不释放CPU,则其他的写成会阻塞住

    1
    2
    3
    4
    5
    6
    7
    8
    for i := 0; i < 10; i++ {
    go func() {
    for {
    fmt.Println("hello go routine: ", i) // 闭包,当外部i变更,函数内部i的值也会随着变化。fmt.Println需要调用io,会发生goroutine切换,切换到main goroutine会终止主进程
    }
    }()
    }
    time.Sleep(1 * time.Second)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    func main() {
    slice1 := make([]int, 11) // 此处需要注意,如果是10,则当i最终加到10的时候,无法访问slice1[10],会出现报错。现在的CPU
    for i := 0; i < 10; i++ {
    go func() {
    for {
    slice1[i]++ // 闭包,外部i改变,影响函数内部,最终i的值是10,当slice1的len是10,则会出现out of range
    }
    }()
    }
    time.Sleep(1 * time.Second)
    }

    去掉闭包的影响

    1
    2
    3
    4
    5
    6
    7
    8
    9
    runtime.GOMAXPROCS(2)   // 指定GOMAXPROCS个数2
    slice1 := make([]int, 10)
    for i := 0; i < 10; i++ {
    go func(i int) {
    for {
    slice1[i]++ // 在go 1.12.17版本中,协程进入死循环,无法被抢占,main goroutine无法被调度,会导致进程阻塞。 ps: 实测1.14.15以及后续版本不会,需要进一步考究golang版本更新。
    }
    }(i)
    }

    解决goroutine死循环无法被调度,可以手动调度,通过runtime.Gosched()

    1
    2
    3
    4
    5
    6
    7
    8
    9
    slice1 := make([]int, 10)
    for i := 0; i < 10; i++ {
    go func(i int) {
    for {
    slice1[i]++
    runtime.Gosched() // 强制该goroutine被调度
    }
    }(i)
    }

竞态检测

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 go run -race goroutine.go
slice1 address: 0xc0000be000==================
WARNING: DATA RACE
Read at 0x00c0000be000 by main goroutine:
Previous write at 0x00c0000be000 by goroutine 7:

for i := 0; i < 10; i++ {
go func(i int) {
for {
slice1[i]++ // write at 0x00c0000be000 by goroutine 7:
runtime.Gosched()
}
}(i)
}
time.Sleep(1 * time.Second)
fmt.Println(slice1) // Read at 0x00c0000be000 by main goroutine:

可以看到变量slice1出现竞争读写,main goroutine 在读,goroutine 在写入

闭包情况下

1
2
3
4
5
6
7
8
9
for i := 0; i < 10; i++ {                 // main goroutine 写i
fmt.Printf("i address: %p", i)
go func() {
for {
slice1[i]++ // goroutine 读i
runtime.Gosched()
}
}()
}

Goroutine 间通信

Goroutine 通过channel实现通信

channel分为有缓冲channel和无缓冲channel

1
2
c := make(chan int)
c <- 1 // fatal error: all goroutines are asleep - deadlock! 无缓冲channel没有协程接收时会发生死锁

channel有方向区分,默认可接受可发送,定义时可定义只能接收或只能发送

1
2
c := make(chan<- int)
d := make(<-chan int)

关闭的channel

1
2
3
ch := make(chan int)
close(ch)
fmt.Printf("get %d from close channel", <-ch) // get 0 from close channel

从关闭的channel种可以一直获取值,获取到的是类型的零值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ch := make(chan int)
go func() {
for {
data, ok := <-ch
if ok {
fmt.Printf("get %d from channel\n", data) // get 1 from channel
} else {
fmt.Printf("get %d from close channel\n", <-ch) // get 0 from close channel
break
}
}
}()
ch <- 1
close(ch)

通过range可以检测到channel关闭

1
2
3
4
5
6
7
8
ch := make(chan int)
go func() {
for data := range ch {
fmt.Printf("get %d from channel\n", data)
}
}()
ch <- 1
close(ch) // 当ch关闭,range也结束完成

chanel不关闭

1
2
3
4
5
6
7
ch := make(chan int)
go func() {
ch <- 1
}()
for data := range ch { // fatal error: all goroutines are asleep - deadlock!
fmt.Printf("get %d from channel\n", data)
}

CSP模型

不通过共享内存通信,通过通信来共享内存。

“使用共享内存通信”:这句话可以理解为对某个变量flag变更,通知其他的goroutine获取到变更

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var flag bool = false     															// 公共变量(共享内存)

func worker(id int, ch chan int) {
for {
data := <-ch
fmt.Printf("from worker %d get %d\n", id, data)
if data == 2 {
flag = true
}
}
}

func main() {
ch := make(chan int)
go worker(0, ch)
ch <- 1
ch <- 2
for {
if flag {
return
}
}
}

通过channel通信

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func worker(id int, ch chan int, reschan chan bool) {
for {
data := <-ch
fmt.Printf("from worker %d get %d\n", id, data)
if data == 2 {
reschan <- true
}
}
}

func main() {
ch := make(chan int)
reschan := make(chan bool)
go worker(0, ch, reschan)
ch <- 1
ch <- 2
<-reschan
}

通过chanel通信的几种情况:

go routine 乱序执行,即使channel有序推送信息,不同的go routine接收也是乱序的,如果要顺序执行,可以借助另外的channel实现

1
2
3
4
5
6
7
8
9
10
11
func WorkerDo(id int, c chan int, done chan struct{}) {
for i := range c {
fmt.Printf("get %c from worker %d\n", i, id) // 先接收
done <- struct{}{} // 再发送
}
}

for i, w := range workers {
w.in <- 'a' + i // 一个发送,发送完成之后,对端有人接收
<-w.done // 一个接收,接收到了之后再处理下一个,实现顺序执行
}

乱序,将发送和接收错开

1
2
3
4
5
6
7
8

for i, w := range workers {
w.in <- 'a' + i // 直接发送,对端接受乱序
}

for _, w := range workers {
<-w.done
}

goroutine间通过channel通信需要注意阻塞问题,当没有缓冲的channel往里面发送,对端没有接受者就会出现阻塞,或者通过for获取channel里面的内容,当没有内容也会出现阻塞,除非channelclose掉。

代码规范中,定义channel有两种情况,一种是有bufferbuffer为1,一种是没有buffer,需要被阻塞住。

sync.Wait

除了使用channel通信通知,官方提供的go routine运行完成之前main goroutine 等待的方法是使用sync.Wait

1
2
3
4
5
6
7
8
9
10
wg := &sync.WaitGroup{}
wg.Add(10) // 10个
wg.Wait() // 等待所有的wg.Done

func WorkerDo(id int, c chan int, wg *sync.WaitGroup) {
for i := range c {
fmt.Printf("get %c from worker %d\n", i, id)
wg.Done()
}
}

一般会将wg封装到对象中,也可以通过函数式编程封装done的功能为一个函数

1
2
3
4
5
6
7
8
9
10
11
12
13
type Worker struct {
id int
in chan int
done func()
}

workers = append(workers, Worker{
id: i,
in: in,
done: func() {
wg.Done()
},
})

select

可以同时从多个channel中接收数据,哪个channel先有数据则先接收哪个。

阻塞式

1
2
3
4
5
6
select {
case n := <-ch1:
fmt.Printf("get %d from ch1\n", n)
case n := <-ch2:
fmt.Printf("get %d from ch2\n", n)
}

通过default非阻塞式

1
2
3
4
5
6
7
8
select {
case n := <-ch1:
fmt.Printf("get %d from ch1\n", n)
case n := <-ch2:
fmt.Printf("get %d from ch2\n", n)
default:
fmt.Println("select default get nothing")
}

一般配合for使用,循环监听

1
2
3
4
5
6
7
for {
select {
case n := <-ch1:
fmt.Printf("get %d from ch1\n", n)
case n := <-ch2:
fmt.Printf("get %d from ch2\n", n)
}

如果监听nil channel,则不会接收数据,也就不会处理对应逻辑。可用于做一些数据准备相关的逻辑。

定时器

select有时候搭配定时器使用,可以设置最长时间、超时、定时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
tm := time.After(time.Duration(5) * time.Second) // 返回一个channel,时间到会往channel发送一个Time数据
tt := time.Tick(time.Duration(1) * time.Second) // 返回一个channel,每隔一段时间往channel里面发送一个Timer数据,可以用于定时器
for {
select {
case n := <-ch1:
fmt.Printf("get %d from ch1\n", n)
case n := <-ch2:
fmt.Printf("get %d from ch2\n", n)
case <-time.After(time.Duration(rand.Intn(400)) * time.Millisecond): // 每次循环定义一个计时器,可用于计算超时
fmt.Println("time out")
case <-tt:
fmt.Println("1s gone")
case <-tm:
fmt.Println("5s gone bye")
return
}
}

同步机制

使用协程算加法

1
2
3
4
5
for i := 0; i < 10000; i++ {
go func() {
sum += int32(i)
}()
}

最后的结果会不一样。go run -race xxx.go可以看到有两处数据竞态,一个是变量i,一个是变量sum

解决sum竞态的的方法有很多,下面一一讨论,先解决闭包的问题。

闭包:

将函数外部的变量通过参数传递到函数内部

1
2
3
4
5
for i := 0; i < 10000; i++ {
go func(i int) {
sum += int32(i)
}(i)
}

原子操作

Golang提供atomic包,atomic.AddInt32是原子操作

1
2
3
4
5
for i := 0; i < 10000; i++ {
go func(i int) {
atomic.AddInt32(&sum, int32(i))
}(i)
}

多个goroutine同时操作一个对象时,通过加锁和解锁解决竞态

1
2
3
4
5
6
7
8
lock := sync.Mutex{}
for i := 0; i < 10000; i++ {
go func() {
lock.Lock()
defer lock.Unlock()
sum++
}()
}

并发控制

阻塞:获取了一个协程的数据之后再处理其他的协程

1
2
fmt.Println("get message: ", <-m1)
fmt.Println("get message: ", <-m2)

非阻塞:通过内部多个协程或者select做非阻塞

1
2
3
4
5
6
7
8
func nonBlockingWait(c chan string) (string, bool) {
select {
case m := <-c:
return m, true
default:
return "", false
}
}

超时机制:在select过程中通过超时时间判断

1
2
3
4
5
6
7
8
func timeoutWait(c chan string, timeout time.Duration) (string, bool) {
select {
case <-time.After(timeout):
return "", false
case m := <-c:
return m, true
}
}

退出,以及优雅的退出,通过done信道获得退出信号。一般情况退出之后主进程无法判断协程是否正常退出,可以通过done信道双向通信。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
go func() {
i := 0
for {
time.Sleep(time.Duration(rand.Intn(2000)) * time.Millisecond)
select {
case <-done:
fmt.Printf("service %s done\n", name)
done <- struct{}{}
fmt.Printf("service %s already done\n", name)
return
default:
c <- fmt.Sprintf("service %s send message %d", name, i)
}
i++
}
}()

通过chanel实现广度优先算法

广度优先的思想,就是一层一层的计算,将计算出的一层的结果放到队列中,从队列中获取这一层的结果进行计算。

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
// 走迷宫

// 走的方向,上,右,下,左
var dirs = [4]point{
{-1, 0},
{0, -1},
{1, 0},
{0, 1},
}

type point struct {
i, j int
}

func (p point) add(r point) point {
return point{
i: p.i + r.i,
j: p.j + r.j,
}
}

func walk(maze [][]int, start point, end point) [][]int {
// 定义路径,跟迷宫一样的数据结构
steps := make([][]int, len(maze))
for i := range steps {
steps[i] = make([]int, len(maze[i]))
}

// 定义队列,初始值是起始位置
// 上下左右最大有四个可以走的
Q := make(chan point, 4)
Q <- start
steps[start.i][start.j] = 1
// 队列只要有值
for len(Q) > 0 {
count := <-Q // 获取当前point
// 如果这个点是终点
if count == end {
break
}
for _, dir := range dirs {
next := count.add(dir) // 计算出来的下一个点
// 下一个点满足这几个条件才可以放到queue中

// 不能超过最外面的一层
if next.j < 0 || next.i < 0 || next.i > len(maze)-1 || next.j > len(maze[next.i])-1 {
continue
}

// 位置值不能是1,不能走在墙上
if maze[next.i][next.j] == 1 {
continue
}

// 位置不能是前面走过来的
if steps[next.i][next.j] != 0 {
continue
}
// 将脚步记录下来
steps[next.i][next.j] = steps[count.i][count.j] + 1
// 这一个点可以走下去,丢到队列中
Q <- next
}
}

return steps
}