一、for 和 range
Go语言提供了for
循环和for...range
循环两种循环结构。
for...range
完成数据迭代,支持字符串、数组、数组指针、切片、字典、通道类型,返回索引、键值数据。
1. 经典循环和范围循环
- 经典循环: 使用
for
关键字和条件语句来控制循环的方式。 - 范围循环: 范围循环是使用
for range
关键字来迭代可迭代的数据结构的方式。范围循环支持字符串、数组、数组指针、切片、字典、通道类型,返回索引、键值数据。
2. for range
不会出现循环永动机
代码语言:javascript复制func main() {
arr := []int{1, 2, 3}
for _, v := range arr {
arr = append(arr, v)
}
fmt.Println(arr)
}
// 输出: 1 2 3 1 2 3
for range
在遍历数组或者切片时,会先将数组或者切片拷贝到一个中间变量ha
, 在赋值的过程中就发生了拷贝, 所以我们遍历的切片已经不是原始的切片变量了, 因此不会出现循环永动机。
3. for k, v := range
中 ,变量v
每一次迭代中被复用
循环中使用的这个变量 v
会在每一次迭代被重新赋值而覆盖,赋值时也会触发拷贝.
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
4. for range
遍历map
时,顺序随机
Go语言中,map
引入了随机数保证遍历的随机性,避免使用者依赖固定的遍历顺序。
Go语言中,map的遍历顺序被设计为随机的,主要是出于两个原因:
- 避免依赖:如果map的遍历顺序是固定的,那么开发者可能会依赖这个顺序来编写代码,这是一种不好的编程习惯。因为map本质上是一个无序的数据结构,它的设计目标是提供快速的查找,而不是保持元素的顺序。如果需要有序的数据结构,应该使用其他的数据结构,如数组或切片。
- 安全性:随机的遍历顺序可以防止某些类型的散列冲突攻击。如果敌手知道map的遍历顺序,他们可能会尝试构造特定的键,以使得散列函数产生冲突,从而导致程序性能下降。通过随机化遍历顺序,这种攻击的可能性被大大降低。
5. for...range
遍历通道channel
在Go语言中,for...range
可以用于遍历通道(channel
)。
这种结构会持续从通道接收值,直到该通道被关闭。它允许我们在不知道通道何时会停止发送数据的情况下,安全地从通道接收数据。
6. 使用for...range
时,常见的错误和陷阱
- 修改迭代变量:在
for...range
循环中,迭代变量实际上是原始集合元素的副本,而不是元素本身。这意味着如果你修改了迭代变量,原始集合不会受到影响。这是一个常见的误解,特别是在遍历数组或切片时。 - 并发修改:在多个goroutine中使用
for...range
遍历并修改同一个集合可能会导致数据竞争。你应该避免这种情况,或者使用适当的同步机制(如互斥锁)来保护数据。 - 关闭已关闭的通道:如果你试图关闭一个已经关闭的通道,Go会抛出
panic
。在使用for...range
遍历通道时,你需要确保通道只被关闭一次。 - 无限循环:如果你在
for...range
循环中向通道发送数据,但忘记关闭通道,那么循环将永远不会结束,因为for...range
会一直等待新的数据。你需要确保在适当的时候关闭通道。 - 忽略
for...range
的第一个返回值:在遍历映射时,for...range
会返回两个值:键和值。如果你只需要值,可能会忽略键,这可能会导致意外的结果。例如,for _, v := range m
和for v := range m
是不同的,前者遍历的是映射的值,后者遍历的是映射的键。 - 字符串遍历:当使用
for...range
遍历字符串时,返回的索引是Unicode字符的起始字节的索引,而不是连续的。如果字符串包含多字节的Unicode字符,这可能会导致混淆。
二、select
1. select
简介
Go语言中select
用于处理多个channel
的发送和接收操作。
这使得我们可以在一个goroutine
中处理多个channel
的数据,极大地提高了并发程序的灵活性和效率。
2. select
的基本用法
代码语言:javascript复制select {
case <-ch1:
// 从ch1接收数据并处理
case ch2 <- value:
// 向ch2发送数据
default:
// 没有可用的case时执行
}
3. select
关键字的工作原理
select
会遍历每一个case
,检查case
中channel
的I/O
操作是否可以立即进行。
- 如果可以立即进行,则执行该
case
。 - 如果有多个
case
都可以进行,则随机选择一个执行。 - 如果没有
case
可以立即进行,且存在default
分支,则执行default
分支;否则,select
将阻塞,直到至少有一个case
可以进行。
4. select
关键字的实际应用场景
- 使用
select
实现多路复用
在并发编程中,我们经常需要同时处理多个channel的数据。使用select,我们可以在一个goroutine中同时监听多个channel,等待channel准备好进行I/O操作。这就是所谓的多路复用。例如:
代码语言:javascript复制select {
case msg1 := <-ch1:
fmt.Println("Received", msg1)
case msg2 := <-ch2:
fmt.Println("Received", msg2)
}
b. 使用select
处理超时
select {
case res := <-ch:
fmt.Println(res)
case <-time.After(1 * time.Second):
fmt.Println("Timeout")
}
c. 使用select
实现非阻塞通信
有时,我们希望channel的发送和接收操作不会阻塞当前的goroutine。使用select和default分支,我们可以实现非阻塞的channel操作。例如:
代码语言:javascript复制select {
case msg := <-ch:
fmt.Println("Received", msg)
default:
fmt.Println("No message received")
}
三、defer
1. defer
简介
在Go语言中,defer关键字用于注册延迟调用。这些调用直到return
前才被执行,通常用于处理成对的操作,如打开、关闭、连接、断开连接、加锁、释放锁等。
defer关键字还可以帮助我们避免在函数执行过程中忘记释放资源或处理错误的问题。
2. defer
的基本用法
代码语言:javascript复制func main() {
file, err := os.Open("file.go")
if err != nil {
log.Fatal(err)
}
defer file.Close()
// ...处理文件
}
3. defer
的工作原理
在Go语言中,defer
关键字的工作原理是基于栈的。
当Go语言执行到一个defer
语句时,不会立刻执行defer
后面的函数,而是将其推入到一个栈中。然后在函数返回前,按照后进先出LIFO
的顺序执行栈中的函数调用。
注意: defer
语句中的函数会在return
语句更新返回值变量后再执行。这意味着你可以在defer
函数中修改返回值。
以下是一个简单的示例:
代码语言:javascript复制func deferExample() (result int) {
defer func() {
result
}()
return 0
}
在这个例子中,虽然return语句返回的是0,但是由于defer语句在return语句后执行,所以最终函数的返回值会变成1。
4. 使用defer
处理错误和异常
我们也可以使用defer
配合recover
函数来捕获和处理运行时的panic
,以实现异常处理。
func safeDivide(numerator, denominator int) (result int) {
defer func() {
if err := recover(); err != nil {
log.Println("panic occurred:", err)
result = 0
}
}()
return numerator / denominator
}
在这个例子中,如果denominator
为0,那么除法操作会引发panic
,但是我们的defer
函数会捕获这个panic
并恢复程序的执行,防止程序崩溃。
四、make
和 new
make
和new
是两个用于内存分配的重要关键字。它们都可以用来创建对象。
1. new
简介
new
是Go语言中的一个内建函数,用于分配内存。它的函数签名为func new(Type) *Type
。
new
函数接受一个类型作为参数,分配足够的内存来容纳该类型的值,并返回指向该内存的指针。
new
关键字的工作原理相对简单。当调用new
函数时,它会在堆上为指定类型分配一块内存,这块内存会被初始化为该类型的零值,然后返回一个指向这块内存的指针。这个指针指向的内存被清零,也就是说,对于所有的类型,new
函数都返回一个指向零值的指针。
ptr := new(int)
fmt.Println(*ptr) // 输出:0
*ptr = 100
fmt.Println(*ptr) // 输出:100
2. make
简介
make
也是Go语言中的一个内建函数,用于分配并初始化下列对象:
- 切片
- 映射
- 通道
make
返回的是初始化的(非零)值,而不是指针。
make
的函数签名取决于它创建的类型。例如,对于切片,函数签名为func make([]T, len, cap) []T
,其中T是切片的元素类型,len是切片的长度,cap是切片的容量。
make
函数的工作原理与new
函数有所不同。当调用make
函数时,它会分配一块内存,初始化该内存,然后返回一个指向该内存的引用。这个引用不是指向零值的指针,而是指向已初始化的值。
// 创建一个长度和容量都为5的切片
slice := make([]int, 5)
fmt.Println(slice) // 输出:[0 0 0 0 0]
// 创建一个映射
m := make(map[string]int)
m["one"] = 1
fmt.Println(m) // 输出:map[one:1]
// 创建一个通道
ch := make(chan int)
go func() {
ch <- 1
}()
fmt.Println(<-ch) // 输出:1
3. make
和new
关键字的比较
- 返回类型:
new
返回的是指向类型零值的指针,而make
返回的是初始化后的(非零)值。 - 使用类型:
new
可以用于任何类型,而make
只能用于切片、映射和通道。 - 零值和初始化:
new
分配的内存被清零,也就是说,对于所有的类型,new函数都返回一个指向零值的指针。而make
则返回一个已初始化的值,而不是零值。