前言
循环控制结构是一种在各种编程语言中常用的程序控制结构,其与顺序控制结构、选择控制结构组成了程序的控制结构,程序控制结构是指以某种顺序执行的一系列动作,用于解决某个问题。理论和实践证明,无论多复杂的算法均可通过顺序、选择、循环3种基本控制结构构造出来。
forgotogoto
本文代码基于Go 1.16版本,不同版本如有差异请见谅
万能的for循环
whiledo..whileforwhiledo..whilefor
forwhiledo..while
func main() {
N := 10
for i := 0; i < N; i++ {
// TODO
}
for {
/*
* while true{
* // TODO
* }
*
*/
}
for N > 10 {
/*
* while N>10 {
* // TODO
* }
*
*/
}
for {
// TODO
// do{
// TODO
// }while(N>10)
if N <= 10 {
break
}
}
}
forwhiledo..while
for i := 0; i < N; i++ {}
for i := 0; i < N; i++ {}mapchannelrangemapchannelmapchannel
遍历数组和切片
遍历数组和切片的方式都是一样的,因为切片的使用概率要大于数组,所以主要讲的切片的遍历,数组可以与其相同方式进行使用,首先 Show me code!
func main() {
slice := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
// first
for range slice {
fmt.Println()
}
// second
for k := range slice {
fmt.Println(k)
}
// third
for k, v := range slice {
fmt.Println(k, v)
}
}
接下来我们挨个的分析一下first,second,third三个注释下面的三种循环方式的区别以及一些特点:
rangerangefor k,_ := range slicerange
range
下面的代码信息全部引用自:go/src/cmd/compile/internal/gc/range.go
优化后代码引自:https://draveness.me/golang/docs/part2-foundation/ch05-keyword/golang-for-range/
首先提前说一下下面会出现的所有变量名的含义,该含义全部引用自编译器源代码。
v1,v2 =>代表 range前变量的索引、数据字段即
for k,v:=range slice{}中的k、v
ha => 被遍历元素的复制版
for k,v:=range slice {}中 slice的复制品
a => 可以理解为被遍历元素
for k,v:=range slice {}中 slice的复制品
即 ha 为 a 的复制版
hn => len(slice) ,即slice的长度
hv1 => 循环内的循环指针,可以理解为
for i := 0; i < N; i++ {}中的 i
忽略索引和数据
range
ha := a
hv1 := 0
hn := len(ha)
v1 := hv1
for ; hv1 < hn; hv1++ {
...
}
range
range
只关心索引
range
ha := a
hv1 := 0
hn := len(ha)
v1 := hv1
for ; hv1 < hn; hv1++ {
v1 = hv1
...
}
for k := range slice
range
关心返回的索引和数据
range
ha := a
hv1 := 0
hn := len(ha)
v1 := hv1
v2 := nil
for ; hv1 < hn; hv1++ {
tmp := ha[hv1]
v1, v2 = hv1, tmp
...
}
for k,v:= range slicetmp
遍历字符串
range
func main() {
str:= "GopherEcho这是我的公众号"
for k, v := range str {
fmt.Print(k, ":", string(v), " ")
}
// out:
// 0:G 1:o 2:p 3:h 4:e 5:r 6:E 7:c 8:h 9:o 10:这 13:是 16:我 19:的 22:公 25:众 28:号
}
range
下列代码摘自:go/src/cmd/compile/internal/gc/range.go 362-375行
ha := a
for hv1 := 0; hv1 < len(ha); {
hv1t := hv1
hv2 := rune(ha[hv1])
if hv2 < utf8.RuneSelf {
hv1++
} else {
hv2, hv1 = decoderune(ha, hv1)
}
v1, v2 = hv1t, hv2
// todo
}
decoderune(ha, hv1)for k, v := range str
decoderune
遍历Map
range
func main() {
m := map[int]int{
1: 2,
3: 4,
4: 5,
}
for k, v := range m {
fmt.Println(k, v)
}
}
跟字符串啥的使用方式没啥区别,都是那玩意儿,不用太在意,我们主要看的是编译器修改后的代码。
runtime.mapiterinitruntime.mapiternextrange
优化后代码引自:https://draveness.me/golang/docs/part2-foundation/ch05-keyword/golang-for-range/
变量名含义:
hit => map的迭代器对象
th => 迭代器的元素类型
t => 元素类型
key => 遍历的map的key
val => 遍历的map的value
ha := a
hit := hiter(n.Type)
th := hit.Type
mapiterinit(typename(t), ha, &hit)
for ; hit.key != nil; mapiternext(&hit) {
// for range m 时 key,val 都没有
// 讲道理哦,我是不知道忽略k,v遍历map有啥用
key := *hit.key // for k := range m 时只有他
val := *hit.val // for k,v := range m 时 key val 都有
}
runtime.mapiterinitruntime.mapiternext
我将剔除一些无用代码,如race相关代码
runtime.mapiterinit
// 传入参数是,map的类型,对应的map,以及迭代器的变量
func mapiterinit(t *maptype, h *hmap, it *hiter) {
// 空map你遍历个p啊
if h == nil || h.count == 0 {
return
}
// 10-21行
// 就是把要遍历的map里面的一些信息
// 赋值给迭代器
it.t = t
it.h = h
it.B = h.B
it.buckets = h.buckets
if t.bucket.ptrdata == 0 {
h.createOverflow()
it.overflow = h.extra.overflow
it.oldoverflow = h.extra.oldoverflow
}
// 这里要注意了
// fastrand() 会随机分配一个哈希值
// 迭代会从这个hash值开始
// 这也是为什么遍历map的时候是随机结果的原因
r := uintptr(fastrand())
// 如果当前桶大于 31-bucketCntBits
// 那么就让随机出来的hash值
// 重新hash 然后做个修改
if h.B > 31-bucketCntBits {
r += uintptr(fastrand()) << 31
}
// 根据随机的hash
// 算出第一个遍历的桶
it.startBucket = r & bucketMask(h.B)
it.offset = uint8(r >> h.B & (bucketCnt - 1))
// 当前的迭代桶
it.bucket = it.startBucket
// 迭代器可以并发初始化
// 但是 只有一个迭代器
if old := h.flags; old&(iterator|oldIterator) != iterator|oldIterator {
atomic.Or8(&h.flags, iterator|oldIterator)
}
mapiternext(it)
}
runtime.mapiterinit
m := map[string]int{"Sunday": 0, "Monday": 1}
for name, value := range m {
// This loop should not assume Sunday will be visited first.
f(name, value)
}
runtime.mapiternext
// 传入参数是哪个迭代器
func mapiternext(it *hiter) {
// 拿出迭代器中保存的那个map
h := it.h
// 判断是否处于并发读写状态
// 如果处于
// boom!
if h.flags&hashWriting != 0 {
throw("concurrent map iteration and map write")
}
// map的类型
t := it.t
// 迭代器初始化的时候选择的哪个桶
bucket := it.bucket
// 当前遍历到的桶
b := it.bptr
// 应该是遍历到哪个tophash了的索引
i := it.i
// 没搞懂
checkBucket := it.checkBucket
next:
// 如果当前遍历的桶是个nil
// 就找看看有没有能遍历的桶
if b == nil {
// 这代表循环结束绕回了最开始迭代的哪个桶
// 结束了 返回
if bucket == it.startBucket && it.wrapped {
// end of iteration
it.key = nil
it.elem = nil
return
}
// 如果当前map正在处于搬迁状态
// 并且 还没搬迁完
if h.growing() && it.B == h.B {
// 那么就从旧桶遍历
oldbucket := bucket & it.h.oldbucketmask()
// 算出来要遍历的旧桶的偏移位置
b = (*bmap)(add(h.oldbuckets, oldbucket*uintptr(t.bucketsize)))
// 看看这桶需不需要遍历
// 就是看看桶里有没有玩意儿
if !evacuated(b) {
checkBucket = bucket
} else {
b = (*bmap)(add(it.buckets, bucket*uintptr(t.bucketsize)))
checkBucket = noCheck
}
} else {
// 如果不处于增长状态 那就遍历正常的桶
b = (*bmap)(add(it.buckets, bucket*uintptr(t.bucketsize)))
checkBucket = noCheck
}
// 看看是否循环一圈了
bucket++
if bucket == bucketShift(it.B) {
bucket = 0
it.wrapped = true
}
i = 0
}
// 找到了可以遍历的桶了
// 开始遍历这个桶
for ; i < bucketCnt; i++ {
// 通过tophash看看对应位置有没有存储的数据
offi := (i + it.offset) & (bucketCnt - 1)
if isEmpty(b.tophash[offi]) || b.tophash[offi] == evacuatedEmpty {
continue
}
// 位偏移算出key
k := add(unsafe.Pointer(b), dataOffset+uintptr(offi)*uintptr(t.keysize))
if t.indirectkey() {
k = *((*unsafe.Pointer)(k))
}
// 位偏移算出value的地址
e := add(unsafe.Pointer(b),
dataOffset+bucketCnt*uintptr(t.keysize)+uintptr(offi)*uintptr(t.elemsize))
// 下面这个判断与是否处于搬迁状态有关
// 如果在搬迁期间就用mapaccessK(t, h, k)取出key,value
// 否则直接操作内存取key和value
if checkBucket != noCheck && !h.sameSizeGrow() {
if t.reflexivekey() || t.key.equal(k, k) {
hash := t.hasher(k, uintptr(h.hash0))
if hash&bucketMask(it.B) != checkBucket {
continue
}
} else {
if checkBucket>>(it.B-1) != uintptr(b.tophash[offi]&1) {
continue
}
}
}
if (b.tophash[offi] != evacuatedX && b.tophash[offi] != evacuatedY) ||
!(t.reflexivekey() || t.key.equal(k, k)) {
it.key = k
if t.indirectelem() {
e = *((*unsafe.Pointer)(e))
}
it.elem = e
} else {
rk, re := mapaccessK(t, h, k)
if rk == nil {
continue
}
it.key = rk
it.elem = re
}
it.bucket = bucket
if it.bptr != b {
it.bptr = b
}
it.i = i + 1
it.checkBucket = checkBucket
return
}
// 遍历溢出桶,如果没有则b == nil
// 进入找桶逻辑
b = b.overflow(t)
i = 0
goto next
}
runtime.mapiternext
- 根据当前迭代器获取到对应的Map,然后根据当前桶是否处于搬迁状态来决定是遍历常规的桶还是遍历旧桶。
- 如果当前处于搬迁状态,那么在遍历对应桶的内部的时候会采用mapaccessK(t, h, k)去获取对应的key,value的地址。
- 如果不处于搬迁状态,则按照普通的位偏移模式去对key和对应的value进行位偏移取址。
遍历Channel
现在就剩下最后一个东西的遍历了,那就是Channel(终于快写完了),Channel的遍历方式其实和Map差不多,但是有一个问题是需要各位注意的,Show me code!
func main() {
channel := make(chan int, 10)
for v := range channel {
fmt.Println(v)
}
}
(不要在意这个代码会死锁就是个演示而已)
range
range
优化后代码引自:https://draveness.me/golang/docs/part2-foundation/ch05-keyword/golang-for-range/
变量名含义:
hb => 可以理解为在通常接收通道信息时 value ,ok := <-channel 中的ok
hv1 => 可以理解为在通常接收通道信息时 value ,ok := <-channel 中的value
v1 => for v := range channel 中的 v
ha := a
hv1, hb := <-ha
for ; hb != false; hv1, hb = <-ha {
v1 := hv1
hv1 = nil
...
}
上述代码是经过编译器转换成传统三段式循环之后的代码,接下来讲解一下:
- 老规矩,先做一份遍历对象的拷贝。
- 使用ok-idom 方式进行Channel数据的接收,此处有可能会阻塞,如果Channel被关闭,则hb==false
- 然后进入循环,把拿到的值拷贝给v1,然后hv1置空,是为了一个提出的issus,在源码中写了注释:
Zero hv1. This prevents hv1 from being the sole, inaccessible reference to an otherwise GC-able value during the next channel receive. See issue 15281
- 然后从2重新开始循环。
坑!有坑!有大坑!
rangerange
相同的玩意儿?
首先,看代码!
func main() {
slice := []int{1, 2, 3, 4, 5, 6}
slicePtr := make([]*int, 0, 10)
for _, v := range slice {
slicePtr = append(slicePtr, &v)
}
for _, v := range slicePtr {
fmt.Print(*v, ",")
}
// out :
// 6,6,6,6,6,6,
}
range
rangerange
func main() {
slice := []int{1, 2, 3, 4, 5, 6}
for k, v := range slice {
fmt.Println(&k, &v)
}
}
那么如何避免这个问题呢?使用一个中间变量即可,正确代码如下:
func main() {
slice := []int{1, 2, 3, 4, 5, 6}
slicePtr := make([]*int, 0, 10)
for _, v := range slice {
temp := v
slicePtr = append(slicePtr, &temp)
}
for _, v := range slicePtr {
fmt.Print(*v, ",")
}
// out :
// 1,2,3,4,5,6,
}
到底能循环多久?
首先看一下代码:
func main() {
slice := []int{1, 2, 3, 4, 5, 6}
for _, v := range slice {
slice = append(slice, v)
}
fmt.Println(slice)
// out:
// [1 2 3 4 5 6 1 2 3 4 5 6]
}
range
性能?
rangerange
测试环境采用:
Go 1.16版本
Goland 2021.1 EAP版本
goos: darwin
goarch: amd64
cpu: Intel® Core™ i5-1038NG7 CPU @ 2.00GHzmemory:16GB
遍历切片/数组的性能
var TestArray [100000]int
var TestSlice = make([]int, 100000, 100000)
func BenchmarkArrayRange(b *testing.B) {
for i := 0; i < b.N; i++ {
for k, v := range TestArray {
// io.Discard 抛弃输出
_, _ = fmt.Fprintln(io.Discard, k, v)
}
}
}
func BenchmarkArrayFor(b *testing.B) {
for i := 0; i < b.N; i++ {
for j := 0; j < len(TestArray); j++ {
// io.Discard 抛弃输出
_, _ = fmt.Fprintln(io.Discard, i, TestArray[i])
}
}
}
func BenchmarkSliceRange(b *testing.B) {
for i := 0; i < b.N; i++ {
for k, v := range TestSlice {
// io.Discard 抛弃输出
_, _ = fmt.Fprintln(io.Discard, k, v)
}
}
}
func BenchmarkSliceFor(b *testing.B) {
for i := 0; i < b.N; i++ {
for j := 0; j < len(TestSlice); j++ {
// io.Discard 抛弃输出
_, _ = fmt.Fprintln(io.Discard, i, TestSlice[i])
}
}
}
range
BenchmarkArrayRange
BenchmarkArrayRange-8 132 8902794 ns/op
BenchmarkArrayFor
BenchmarkArrayFor-8 183 6219940 ns/opBenchmarkSliceRange
BenchmarkSliceRange-8 136 8848273 ns/op
BenchmarkSliceFor
BenchmarkSliceFor-8 182 6483446 ns/op
rangerange
遍历Map
rangerange
var TestMap = make(map[int]int, 100000)
func init() {
for i := 0; i < 100000; i++ {
reRand:
key, value := rand.Int(), rand.Int()
if _, ok := TestMap[key]; ok {
goto reRand
} else {
TestMap[key] = value
}
}
}
func BenchmarkRange(b *testing.B) {
for i := 0; i < b.N; i++ {
for k, v := range TestMap {
_, _ = fmt.Fprintln(io.Discard, k, v)
}
}
}
同样是100000级别的数据量,测试启动时随机分配,接下来看一下性能表现:
BenchmarkRange
BenchmarkRange-8 62 17623282 ns/op
range
总结
range
我已开通自己的公众号【Echo的技术笔记】
日后的文章发布会主要在公众号上发布
希望各位关注一下
谢谢大家啦