常用关键字
# for和range
# for循环的三种用法
for range用于遍历切片,数组或者map
# 几个用法
- 使用
for range a {}
遍历数组和切片,不关心索引和数据的情况; - 使用
for i := range a {}
遍历数组和切片,只关心索引的情况; - 使用
for i, elem := range a {}
遍历数组和切片,关心索引和数据的情况;
# for range里面修改数组后不会改变遍历次数
func main() {
arr := []int{1, 2, 3}
for _, v := range arr {
arr = append(arr, v)
}
fmt.Println(arr)
}
$ go run main.go
1 2 3 1 2 3
2
3
4
5
6
7
8
9
这里for代码只会执行3次,为什么?
因为我们在遍历时go会把切片赋值给一个新变量,同时通过len
预先获取切片长度,所以不会改变循环次数
# for range使用指针问题
func main() {
arr := []int{1, 2, 3}
newArr := []*int{}
for _, v := range arr {
newArr = append(newArr, &v)
}
for _, v := range newArr {
fmt.Println(*v)
}
}
$ go run main.go
3 3 3
2
3
4
5
6
7
8
9
10
11
12
为什么会这样?
因为go遍历时会额外新建V2
来存切片中的元素,而且每次循环时会重新覆盖这个值,所以结果会相同。正确的做法应该是使用 &arr[i]
替代 &v
# for range清空数组的代码会进行优化
func main() {
arr := []int{1, 2, 3}
for i, _ := range arr {
arr[i] = 0
}
}
2
3
4
5
6
依次遍历切片和哈希看起来是非常耗费性能的,因为数组、切片和哈希占用的内存空间都是连续的,go会对上面这种代码进行优化,会直接清空这片内存中的内容
# map会随机遍历
func main() {
hash := map[string]int{
"1": 1,
"2": 2,
"3": 3,
}
for k, v := range hash {
println(k, v)
}
}
2
3
4
5
6
7
8
9
10
这段代码,每次执行的结果都不一样,因为go会生成随机数,随机选择一个桶来作为起始地址进行遍历。
详细参考:Go 语言 for 和 range 的实现 | Go 语言设计与实现 (draveness.me) (opens new window)
# select
select
是操作系统中的系统调用,我们经常会使用select
、poll
和epoll
等函数构建 I/O 多路复用模型提升程序的性能。Go 语言的select
与操作系统中的select
比较相似,同样用于监听IO多路操作
# 简单例子
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
}
}
}
2
3
4
5
6
7
8
9
10
11
12
上述控制结构会等待 c <- x
或者 <-quit
两个表达式中任意一个返回。无论哪一个表达式返回都会立刻执行 case
中的代码,当 select
中的两个 case
同时被触发时,会随机执行其中的一个。
# 特性
- 当
select
中的两个case
同时被触发时,会随机执行其中的一个 - 当
select
不加default时,就会阻塞当前 Goroutine 直到有一个case可以收发;当添加 default 时,就不会阻塞 Goroutine ,会自动选择一个可以收发的 Channel ,如果没有就执行default的内容。 - 如果
select
控制结构中包含default
语句,有下面两种情况- 当存在可以收发的 Channel 时,直接处理该 Channel 对应的
case
; - 当不存在可以收发的 Channel 时,执行
default
中的语句;
- 当存在可以收发的 Channel 时,直接处理该 Channel 对应的
深入可以参考:Go 语言 select 的实现原理 | Go 语言设计与实现 (draveness.me) (opens new window)
# defer
defer
会在当前函数返回前执行传入的函数,它会经常被用于关闭文件描述符、关闭数据库连接以及解锁资源。
# 多个defer的执行顺序
func main() {
for i := 0; i < 5; i++ {
defer fmt.Println(i)
}
}
$ go run main.go
4
3
2
1
0
2
3
4
5
6
7
8
9
10
11
12
我们可以看到,越到后面的defer会越先执行
深入可参考:理解 Go 语言 defer 关键字的原理 | Go 语言设计与实现 (draveness.me) (opens new window)
# return和defer之间的关系
Go 的函数返回值是通过堆栈返回的, return 语句不是原子操作,而是被拆成了两步.
- 给返回值赋值 (rval)
- 调用 defer 表达式
- 返回给调用函数(ret)
下面这个例子就很好的说明了整个return执行的流程
package main
import "fmt"
func main() {
fmt.Println(increase(1))
}
func increase(d int) (ret int) {
defer func() {
ret++
}()
return d
}
// 函数会返回2
2
3
4
5
6
7
8
9
10
11
12
# defer表达式的返回值会被丢弃
闭包与匿名函数.
- 匿名函数:没有函数名的函数。
- 闭包:可以使用另外一个函数作用域中的变量的函数。
在实际开发中,defer 的使用经常伴随着闭包与匿名函数的使用
package main
import "fmt"
func main() {
for i := 0; i < 5; i++ {
defer func() {
fmt.Println(i)
}()
}
}
/**
5
5
5
5
5
之所以这样是因为,defer 表达式中的 i 是对 for 循环中 i 的引用。到最后,i 加到 5,故最后全部打印 5。
如果将 i 作为参数传入 defer 表达式中,在传入最初就会进行求值保存,只是没有执行延迟函数而已。
**/
/**比如下面这种情况**/
// 这个函数会返回1,因为defer执行的比return更晚,当return返回0时,此时执行defer,然后defer就会对result进行++操作
func f1() (result int) {
defer func() {
result++
}()
return 0
}
// 这个函数会返回5,同样是因为defer执行的更晚
func f2() (r int) {
t := 5
defer func() {
// 这里并不会修改返回值,除非我们修改r
t = t + 5
}()
return t
}
// 你也可以这样理解
func f() (r int) {
t := 5
r = t // 赋值指令
func() { // defer被插入到赋值与返回之间执行,这个例子中返回值r没被修改过
t = t + 5
}
return // 空的return指令
}
// 这个函数会返回1,因为defer里面的r其实是一个局部变量,++操作不会影响返回的值
func f3() (r int) {
defer func(r int) {
r = r + 5
}(r)
return 1
}
// 换成下面这个就会返回6了
func f3() (r int) {
defer func() {
r = r + 5
}()
return 1
}
// 也可以这样理解
func f() (r int) {
r = 1 // 给返回值赋值
func(r int) { // 这里改的r是传值传进去的r,不会改变要返回的那个r值
r = r + 5
}(r)
return // 空的return
}
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
68
69
70
# panic和recover
panic
能够改变程序的控制流,调用panic
后会立刻停止执行当前函数的剩余代码,并在当前 Goroutine 中递归执行调用方的defer
;recover
可以中止panic
造成的程序崩溃。它是一个只能在defer
中发挥作用的函数,在其他作用域中调用不会发挥作用;
# 现象
panic
只会触发当前 Goroutine 的defer
;recover
只有在defer
中调用才会生效;panic
允许在defer
中嵌套多次调用;
深入参考:Go 语言 panic 和 recover 的原理 | Go 语言设计与实现 (draveness.me) (opens new window)
# make和new
make
的作用是初始化内置的数据结构,也就是我们在前面提到的切片、哈希表和 Channel2 (opens new window);new
的作用是根据传入的类型分配一片内存空间并返回指向这片内存空间的指针3 (opens new window);
参考:Go 语言中的 make 和 new | Go 语言设计与实现 (draveness.me) (opens new window)
# switch
go的switch的类型判断部分的表达式是可加可不加的,如果不加就需要在case里面进行判断
switch {
case grade == "A" :
fmt.Printf("优秀!" )
case grade == "B", grade == "C" :
fmt.Printf("良好" )
case grade == "D" :
fmt.Printf("及格" )
case grade == "F":
fmt.Printf("不及格" )
default:
fmt.Printf("差" );
}
2
3
4
5
6
7
8
9
10
11
12
case里面可以有一个或者多个表达式
switch marks {
case 90: grade = "A"
case 80: grade = "B"
case 50,60,70 : grade = "C"
default: grade = "D"
}
2
3
4
5
6
case里是不需要用break的,因为go默认会加break。如果我们不想用break,那么可使用 fallthrough
关键词来执行下面的case代码块
# goto
go是支持goto语句的,不过一般不推荐使用
package main
import "fmt"
func main() {
/* 定义局部变量 */
var a int = 10
/* 循环 */
LOOP: for a < 20 {
if a == 15 {
/* 跳过迭代 */
a = a + 1
goto LOOP
}
fmt.Printf("a的值为 : %d", a)
a++
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# delete
delete
关键字用于删除哈希表 map 中的元素, 参数为 map 和其对应的 key