转载:https://www.luozhiyun.com/archives/213
人是否会进步以及进步得有多快,依赖的恰恰就是对自我的否定,这包括否定的深刻与否,以及否定自我的频率如何。这其实就是“不破不立”这个词表达的含义。
数组和切片的range子句遍历
切片
先看例子:
numbers1 := []int{1, 2, 3, 4, 5, 6}
for i := range numbers1 {
if i == 3 {
numbers1[i] += i
}
}
fmt.Println(numbers1)
在上面这个例子中,会返回:
[1 2 3 7 5 6]
由于numbers1是⼀个 切⽚,那么迭代变量就可以有两个,右边的迭代变量代表当次迭代对应的某⼀个元素值,⽽左边的迭代变量则代表该元素值在切⽚中的索引值。
如果像上面这样只有一个迭代变量只会返回对应的索引值,这里和java很不一样,在java中使用foreach迭代只会返回元素值。
如果像如下这样又会返回什么呢:
numbers3 := []int{1, 2, 3, 4, 5, 6}
maxIndex3 := len(numbers2) - 1
for i, e := range numbers3 {
if i == maxIndex3 {
numbers3[0] += e
} else {
numbers3[i+1] += e
}
}
fmt.Println(numbers3)
在这个例子中,迭代返回了两个参数,i表示的是索引值,e表示的是切片中的元素。
这里打印的结果是:
[22 3 6 10 15 21]
因为切片是引用类型的,所以传入for循环中的是一个指针,所以每次遍历都会修改原来的值。
数组
numbers2 := [...]int{1, 2, 3, 4, 5, 6}
maxIndex2 := len(numbers2) - 1
for i, e := range numbers2 {
if i == maxIndex2 {
numbers2[0] += e
} else {
numbers2[i+1] += e
}
}
fmt.Println(numbers2)
像上面这样遍历一个数组,那么会返回什么?
[7 3 5 7 9 11]
因为数组是值类型的,所以range表达式的求值结果会被复制,也就是说,被迭代的对象是range表达式结果值的副本⽽不是原值。
在第⼀次迭代时,我改变的是numbers2的第⼆个元素的值,新值为3,也就是1和2之和。
但是,被迭代的对象的第⼆个元素却没有任何改变,毕竟它与numbers2已经是毫不相关的两个数组了。因此,在第⼆次迭代 时,我会把numbers2的第三个元素的值修改为5,即被迭代对象的第⼆个元素值2和第三个元素值3的和。
异常处理
panic
panic类似java里面的Exception,当引发一个panic的时候会将调用栈打印出来,并引发程序的崩溃终止。
panic详情会在控制权传播的过程中,被逐渐地积累和完善,并且,控制权会一级一级地沿着调用栈的反方向传播至顶端。
因此,在针对某个 goroutine 的代码执行信息中,调用栈底端的信息会先出现,然后是上一级调用的信息,以此类推,最后才是此调用栈顶端的信息。
例如下面这个例子:
main函数调用了caller1函数,而caller1函数又调用了caller2函数,那么caller2函数中代码的执行信息会先出现,然后是caller1函数中代码的执行信息,最后才是main函数的信息。
recover函数和defer语句
Go 语言的内建函数recover专用于恢复 panic,或者说平息运行时恐慌。recover函数无需任何参数,并且会返回一个空接口类型的值。
我们在使用recover 函数的时候不能将其放置在panic之后或之前。
放置之后panic 一旦发生,控制权就会讯速地沿着调用栈的反方向传播。所以,在panic函数调用之后的代码,根本就没有执行的机会;把调用recover函数的代码提前,那时未发生 panic,那么该函数就不会做任何事情,并且只会返回一个nil。
所以我们需要将recover函数和defer语句配合使用,defer语句相当于java里面的try -finally,它会让其携带的defer函数的调用延迟执行,并且会延迟到该defer语句所属的函数即将结束执行的那一刻。
利用recover和defer防止由panic引起的程序崩溃
func main() {
fmt.Println("Enter function main.")
defer func(){
fmt.Println("Enter defer function.")
if p := recover(); p != nil {
fmt.Printf("panic: %s\n", p)
}
fmt.Println("Exit defer function.")
}()
// 引发panic。
panic(errors.New("something wrong"))
fmt.Println("Exit function main.")
}
在上面这个代码中会返回:
Enter function main.
Enter defer function.
panic: something wrong
Exit defer function.
在defer语句中,通过recover函数将错误给成功捕获,避免了程序的崩溃。
测试
- 对于功能测试函数来说,其名称必须以Test为前缀,并且参数列表中只应有一个*testing.T类型的参数声明。
- 对于性能测试函数来说,其名称必须以Benchmark为前缀,并且唯一参数的类型必须是*testing.B类型的。
功能测试
$ go test lesson2/article10/main
ok lesson2/article10/main 0.006s
如上,我们执行一个功能测试,会返回一个ok,表示测试成功,并且会显示在0.006s完成了测试。
如果我们再次的执行
$ go test lesson2/article10/main
ok lesson2/article10/main (cached)
会发现多了个(cached),这表明我们的测试结果被缓存了,但是如果我们的程序有任何变动,缓存数据就会失效,go 命令就会再次真正地执行操作。所以我们并不用担心打印出的缓存数据不是实时的结果。
go 命令会定期地删除最近未使用的缓存数据,但是,如果你想手动删除所有的缓存数据,运行一下go clean -cache命令就好了。
对于测试成功的结果,go 命令也是会缓存的。运行go clean -testcache将会删除所有的测试结果缓存。不过,这样做肯定不会删除任何构建结果缓存。
性能测试
性能测试这个在java中是没有中,只能依靠其他的手段实现。所以我们来看看Go中性能测试是如何做的。
我们写一个测试函数:
func BenchmarkGetPrimes(b *testing.B) {
for i := 0; i < b.N; i++ {
GetPrimes(1000)
}
}
在这个函数中调用了GetPrimes方法,并传入一个1000的值。
我们再运行上面的测试函数如下:
$ go test -bench=. -run=^$ lesson2/article10/main
goos: darwin
goarch: amd64
pkg: lesson2/article10/main
BenchmarkGetPrimes-8 312973 3307 ns/op
PASS
ok lesson2/article10/main 1.082s
我们在上面的命令中加入了两个参数:
-bench=.,只有有了这个标记,命令才会进行性能测试,.表示需要执行任意名称的性能测试函数。
-run=$,这个标记用于表明需要执行哪些功能测试函数,值$意味着:只执行名称为空的功能测试函数,换句话说,不执行任何功能测试函数。
在运行完命令后的结果中:
BenchmarkGetPrimes-8被称为单个性能测试的名称,它表示命令执行了性能测试函数BenchmarkGetPrimes,并且当时所用的最大 P 数量为8。最大 P 数量相当于可以同时运行 goroutine 的逻辑 CPU 的最大个数。
在写测试函数的时候会调用传入参数b *testing.B 的b.N。go test命令会先尝试把b.N设置为1,然后执行测试函数。
如果测试函数的执行时间没有超过上限,此上限默认为 1 秒,那么命令就会改大b.N的值,然后再次执行测试函数,如此往复,直到这个时间大于或等于上限为止。
所以b.N就是指的上面结果中的312973。3307 ns/op表明单次执行GetPrimes函数的平均耗时为3307纳秒。
放上一个示例图看一下:
条件变量sync.Cond
sync.Cond类似于java里面的Condition,初始化的时候也是需要互斥锁初始化。
条件变量提供的方法有三个:等待通知(wait)、单发通知(signal)和广播通知(broadcast)。
等待通知的时候需要在互斥锁保护下进行,但是在signal和broadcast却是需要在对应的互斥锁解锁之后再做这两种操作。
sync.Cond类型并不是开箱即用的。我们只能利用sync.NewCond函数创建它的指针值。这个函数需要一个sync.Locker类型的参数值。
因此我们通常需要这样进行初始化:
var lock sync.Mutex
cond := sync.NewCond(&lock)
在为sendCond变量做初始化的时候,把基于lock变量的指针值传给了sync.NewCond函数。
我们下面来看一个例子,比较长,需要点耐心:
func main() {
// mailbox 代表信箱。
// 0代表信箱是空的,1代表信箱是满的。
var mailbox uint8
// lock 代表信箱上的锁。
var lock sync.Mutex
// sendCond 代表专用于发信的条件变量。
sendCond := sync.NewCond(&lock)
// recvCond 代表专用于收信的条件变量。
recvCond := sync.NewCond(&lock)
// send 代表用于发信的函数。
send := func(id, index int) {
lock.Lock()
for mailbox == 1 {
sendCond.Wait()
}
log.Printf("sender [%d-%d]: the mailbox is empty.",
id, index)
mailbox = 1
log.Printf("sender [%d-%d]: the letter has been sent.",
id, index)
lock.Unlock()
recvCond.Broadcast()
}
// recv 代表用于收信的函数。
recv := func(id, index int) {
lock.Lock()
for mailbox == 0 {
recvCond.Wait()
}
log.Printf("receiver [%d-%d]: the mailbox is full.",
id, index)
mailbox = 0
log.Printf("receiver [%d-%d]: the letter has been received.",
id, index)
lock.Unlock()
sendCond.Signal() // 确定只会有一个发信的goroutine。
}
// sign 用于传递演示完成的信号。
sign := make(chan struct{}, 3)
max := 6
go func(id, max int) { // 用于发信。
defer func() {
sign <- struct{}{}
}()
for i := 1; i <= max; i++ {
time.Sleep(time.Millisecond * 500)
send(id, i)
}
}(0, max)
go func(id, max int) { // 用于收信。
defer func() {
sign <- struct{}{}
}()
for j := 1; j <= max; j++ {
time.Sleep(time.Millisecond * 200)
recv(id, j)
}
}(1, max/2)
go func(id, max int) { // 用于收信。
defer func() {
sign <- struct{}{}
}()
for k := 1; k <= max; k++ {
time.Sleep(time.Millisecond * 200)
recv(id, k)
}
}(2, max/2)
<-sign
<-sign
<-sign
}
在上面的例子中,使用了两个condition,sendCond和recvCond。send函数用于发信的函数,因为我在这里收信方设置了两个goroutine,所以在发送方使用了Broadcast,而接收方使用了Signal。
看完了上面的代码,在看看下面的几个注意事项
先加锁再调用Wait
我们下面来看看Wait方法做了什么:
- 把调用它的 goroutine(也就是当前的 goroutine)加入到当前条件变量的通知队列中。
- 解锁当前的条件变量基于的那个互斥锁。
- 让当前的 goroutine 处于等待状态,等到通知到来时再决定是否唤醒它。此时,这个 goroutine 就会阻塞在调用这个Wait方法的那行代码上。
- 如果通知到来并且决定唤醒这个 goroutine,那么就在唤醒它之后重新锁定当前条件变量基于的互斥锁。自此之后,当前的 goroutine 就会继续执行后面的代码了。
所以Wait方法实际上经历了:
入队 -> 解锁 -> 等待 -> 唤醒 -> 重新加锁 -> 继续执行
所以在阻塞当前的goroutine之前会解锁,所以如果不先进行锁定的话会引发一个不可恢复的 panic。
如果当前的 goroutine 无法解锁,别的 goroutine 也都不来解锁,那么又由谁来进入临界区,并改变共享资源的状态呢?
要用for语句来包裹调用其Wait方法的表达式
这主要是为了保险起见。如果一个 goroutine 因收到通知而被唤醒,但却发现共享资源的状态,依然不符合它的要求,那么就应该再次调用条件变量的Wait方法,并继续等待下次通知的到来。
还有就是为了防止:
- 有多个 goroutine 在等待共享资源的同一种状态。被唤醒的时候无法保证每次的共享资源的状态都是正确的,所以需要做一个检查
- 共享资源可能有的状态可能有很多。所以,在设计合理的前提下,单一的结果一定不可能满足所有 goroutine 的条件。那些未被满足的 goroutine 显然还需要继续等待和检查。
- 在一些多 CPU 核心的计算机系统中,即使没有收到条件变量的通知,调用其Wait方法的 goroutine 也是有可能被唤醒的。这是由计算机硬件层面决定的。
Signal方法和Broadcast方法
条件变量的Wait方法总会把当前的 goroutine 添加到通知队列的队尾,而它的Signal方法总会从通知队列的队首开始,查找可被唤醒的 goroutine。所以,因Signal方法的通知,而被唤醒的 goroutine 一般都是最早等待的那一个。
所以如果能确定只有一个goroutine在等待通知,或者只需要唤醒任意一个goroutine就可以满足要求,那么可以使用Signal,其他情况最好使用Broadcast
sync.WaitGroup
WaitGroup其实有点像java的CountDownLatch,可以用来控制goroutine之间协作流程。
WaitGroup类型拥有三个指针方法:Add、Done和Wait。
例如:
func coordinateWithWaitGroup() {
var wg sync.WaitGroup
wg.Add(2)
num := int32(0)
fmt.Printf("The number: %d [with sync.WaitGroup]\n", num)
max := int32(10)
go addNum(&num, 3, max, wg.Done)
go addNum(&num, 4, max, wg.Done)
wg.Wait()
}
// addNum 用于原子地增加numP所指的变量的值。
func addNum(numP *int32, id, max int32, deferFunc func()) {
defer func() {
deferFunc()
}()
for i := 0; ; i++ {
currNum := atomic.LoadInt32(numP)
if currNum >= max {
break
}
newNum := currNum + 2
time.Sleep(time.Millisecond * 200)
if atomic.CompareAndSwapInt32(numP, currNum, newNum) {
fmt.Printf("The number: %d [%d-%d]\n", newNum, id, i)
} else {
fmt.Printf("The CAS operation failed. [%d-%d]\n", id, i)
}
}
}
coordinateWithWaitGroup函数会等待两个addNum函数运行完wg.Done方法后wg.Wait()才会继续放行。
由于主goroutine需要等待其他的goroutine运行完毕后才能继续往下执行,所以可以在addNum的defer函数中才调用wg.Done函数。
需要注意的是,和java的CountDownLatch不一样的是CountDownLatch的countDown方法是可以重复调用,即使计数小于零也不会报错,但是WaitGroup的计数器的值不能小于0,是因为这样会引发一个 panic。
sync.Once
该方法的功能是只执行“首次被调用时传入的”那个函数,并且之后不会再执行任何参数函数。
举例:
var counter uint32
var once sync.Once
once.Do(func() {
atomic.AddUint32(&counter, 2)
})
fmt.Printf("The counter: %d\n", counter)
once.Do(func() {
atomic.AddUint32(&counter, 1)
})
fmt.Printf("The counter: %d\n", counter)
在上面这个例子中,once.Do里面修饰的函数只会执行第一个,第二个函数并不会得到执行。
因为,Once类型中有一个名叫done的uint32类型的字段。它的作用是记录其所属值的Do方法被调用的次数。不过,该字段的值只可能是0或者1。一旦Do方法的首次调用完成,它的值就会从0变为1。
所以每次在执行once.Do的时候都会判断done是不是为1,并且在这个条件判断之后,Do方法会立即锁定其所属值中的那个sync.Mutex类型的字段m。然后,它会在临界区中再次检查done字段的值,并且仅在条件满足时,才会去调用参数函数,以及用原子操作把done的值变为1。
以上保证了sync.Once只会调用一次。
需要注意的是:
- 由于Do方法里面加了互斥锁,所以如果有多个goroutine同时执行,并且里面参数函数需要执行很长时间,那么其他goroutine会一起阻塞
- Do方法在参数函数执行结束后,对done字段的赋值用的是原子操作,并且,这一操作是被挂在defer语句中的。因此,不论参数函数的执行即使报错,done字段的值都会变为1。
|
请发表评论