面试官(for 这道题尽然难道了所有人)

本篇文章基于 Golang 1.17.2
话说胖虎上次没有问到实习生,觉得实习生底子不错,最近闲来无事,决定在考考实习生。
for 循环的奇怪现象 胖虎:以下代码输出什么
package mainimport "fmt"func main() { s := []int{0, 1} for num := 0; num < len(s); num++ { s = append(s, s[num]) } fmt.Printf("s的值是:%+v\n", s) }

实习生吐口而出:
[0 1 0 1]

胖虎笑着说:不要着急回答,思考三秒后在说出你的答案。
实习生: 难道不对?
胖虎看着实习生疑惑的表情,说:我们来执行下吧
面试官(for 这道题尽然难道了所有人)
文章图片

竟然是死循环!!!实习生差点喊出来,同时发现胖虎奋斗多年的笔记本,风扇吭吭唧唧不情愿的开始干活了。
胖虎:如果想要 0 1 0 1,应该怎么改呢?
实习生:难道用range?边说边敲下以下代码。
package mainimport "fmt"func main() { s := []int{0, 1} for _, value := range s { s = append(s, value) } fmt.Printf("s的值是:%+v\n", s) }

胖虎:那你知道为什么吗?
实习生:范围循环在迭代过程中,难道是迭代值的拷贝?我再试下吧。
package mainimport "fmt"func main() { s := []int{0, 1} for _, value := range s { value += 10 } fmt.Printf("s的值是:%+v\n", s) }

实习生:跟我猜想的一样。
面试官(for 这道题尽然难道了所有人)
文章图片

胖虎点点头:范围遍历在开始遍历数据之前,会先拷贝一份被遍历的数据,所以在遍历过程中去修改被遍历的数据,只是修改拷贝的数据,不会影响到原数据。
而普通for循环,会一直不断追加数据到切片,对原数据产生影响。
胖虎:顺便复习一下这两个的使用语法吧
for 使用语法
s := "test" // 常见的 for 循环,支持初始化语句。 for i, n := 0, len(s); i < n; i++ { println(string(s[i])) }initNum := 0 intSlice := []int{0, 1} sliceLen := len(intSlice) for initNum < sliceLen { initNum++ fmt.Println(intSlice[initNum]) }for {// 替代 while (true) {} println(s)// 替代 for (; ; ) {} }

for 常见使用场景 由此可见for常见的使用场景是字符串,数组,切片和无限循环。但需要注意的是 for 循环字符串的时候,结果想要为字符串的时候,需要string转换一下,而有的编程语言不需要,比如说世界上最好的语言。
面试官(for 这道题尽然难道了所有人)
文章图片

当然使用 range 遍历字符串就没有这个问题。
range 使用语法
words := []string{"Go", "Java", "C++"} for i, value := range words { words = append(words, "test") fmt.Println(i, value) }test := "abc" // 忽略 value for i := range s { fmt.Println(s[i]) }

range 常见使用场景 Go 语言中,range 使用场景除了数组(array)、切片(slice),还可以很方便字典(map)和信道(chan)
遍历map
m := map[string]int{ "one":1, "two":2, "three": 3, } for k, v := range m { delete(m, "two") m["four"] = 4 fmt.Printf("key:%v, value:%v\n", k, v) }

输出结果:
key:two, value:2 key:three, value:3 key:one, value:1

【面试官(for 这道题尽然难道了所有人)】需要注意的是:
  • 和切片不同的是,迭代过程中,删除还未迭代到的键值对,则该键值对不会被迭代。
  • 在迭代过程中,如果创建新的键值对,那么新增键值对,可能被迭代,也可能不会被迭代。
  • 针对 nil 字典,迭代次数为 0
遍历channel
ch := make(chan string) go func() { ch <- "听我说" ch <- "谢谢你" ch <- "因为有你" ch <- "温暖了四季" close(ch) }() for n := range ch { fmt.Println(n) }

结果如下:
听我说 谢谢你 因为有你 温暖了四季

range 循环性能一定比for差吗? []int
package mainimport ( "math/rand" "testing" "time" )func generateWithCap(n int) []int { //生成不同系列的随机数 rand.Seed(time.Now().UnixNano()) nums := make([]int, 0, n) for i := 0; i < n; i++ { nums = append(nums, rand.Int()) } return nums }func BenchmarkForIntSlice(b *testing.B) { nums := generateWithCap(1024 * 1024) for i := 0; i < b.N; i++ { len := len(nums) var tmp int for k := 0; k < len; k++ { tmp = nums[k] } _ = tmp } }func BenchmarkRangeIntSlice(b *testing.B) { nums := generateWithCap(1024 * 1024) for i := 0; i < b.N; i++ { var tmp int for _, num := range nums { tmp = num } _ = tmp } }

执行结果如下:
goos: darwin goarch: amd64 cpu: Intel(R) Core(TM) i5-1038NG7 CPU @ 2.00GHz BenchmarkForIntSlice-83552334038 ns/op BenchmarkRangeIntSlice-83544321965 ns/op

名词解释:
BenchmarkForIntSlice-8,即 GOMAXPROCS,默认等于 CPU 核数。
3552 代表运行了多少次
334038 ns/op 每次执行平均时间,
由此可见 ,遍历 []int 类型的切片,for 与 range 性能两者几乎没有区别。
[]struct
如果是稍微复杂一点的 []struct 类型呢?
type Item struct { idint val [4096]byte }//for 循环 func BenchmarkForStruct(b *testing.B) { var items [1024]Item for i := 0; i < b.N; i++ { length := len(items) var tmp int for k := 0; k < length; k++ { tmp = items[k].id } _ = tmp } }//range 循环只取下标 func BenchmarkRangeIndexStruct(b *testing.B) { var items [1024]Item for i := 0; i < b.N; i++ { var tmp int for k := range items { tmp = items[k].id } _ = tmp } }//range 循环取值 func BenchmarkRangeStruct(b *testing.B) { var items [1024]Item for i := 0; i < b.N; i++ { var tmp int for _, item := range items { tmp = item.id } _ = tmp } }

执行结果如下:
goos: darwin goarch: amd64 cpu: Intel(R) Core(TM) i5-1038NG7 CPU @ 2.00GHz BenchmarkForStruct-83373856359.7 ns/op BenchmarkRangeIndexStruct-83587757329.0 ns/op BenchmarkRangeStruct-83478288161 ns/op

  • 仅遍历下标的情况下,for 和 range 的性能几乎是一样的。
  • items 的每一个元素的类型是一个结构体类型 ItemItem 由两个字段构成,一个类型是 int,一个是类型是 [4096]byte,也就是说每个 Item 实例需要申请约 4KB 的内存。
  • 在这个例子中,for 的性能大约是 range (同时遍历下标和值) 的 2000 倍。
[]*struct{}
func generateItems(n int) []*Item { items := make([]*Item, 0, n) for i := 0; i < n; i++ { items = append(items, &Item{id: i}) } return items }//for 循环指针 func BenchmarkForPointer(b *testing.B) { items := generateItems(1024) for i := 0; i < b.N; i++ { length := len(items) var tmp int for k := 0; k < length; k++ { tmp = items[k].id } _ = tmp } }//range 循环指针 func BenchmarkRangePointer(b *testing.B) { items := generateItems(1024) for i := 0; i < b.N; i++ { var tmp int for _, item := range items { tmp = item.id } _ = tmp } }

执行结果:
goos: darwin goarch: amd64 cpu: Intel(R) Core(TM) i5-1038NG7 CPU @ 2.00GHz BenchmarkForPointer-87349051545 ns/op BenchmarkRangePointer-87097481558 ns/op

切片元素从结构体 Item 替换为指针 *Item 后,for 和 range 的性能几乎是一样的。而且使用指针还有另一个好处,可以直接修改指针对应的结构体的值。
所以说for 不一定比range快。
实习生:那我总结一下吧。
面试官(for 这道题尽然难道了所有人)
文章图片

胖虎:总结的不错,下次不要总结了。
实习生:那我走?
胖虎:哈哈哈哈,开玩笑的
参考文章:https://geektutu.com/post/hpg...
本文由博客一文多发平台 OpenWrite 发布!

    推荐阅读