2025年4月21日 星期一 乙巳(蛇)年 正月廿二 设为首页 加入收藏
rss
您当前的位置:首页 > 计算机 > 编程开发 > Go语言

面试官:for 这道题难倒了所有人

时间:07-30来源:作者:点击数:55

话说胖虎上次没有问到实习生,觉得实习生底子不错,最近闲来无事,决定在考考实习生。

for 循环的奇怪现象

胖虎:以下代码输出什么

  • package main
  • import "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]

胖虎笑着说:不要着急回答,思考三秒后在说出你的答案。

实习生: 难道不对?

胖虎看着实习生疑惑的表情,说:我们来执行下吧

image-20220420143721096

竟然是死循环!!!实习生差点喊出来,同时发现胖虎奋斗多年的笔记本,风扇吭吭唧唧不情愿的开始干活了。

胖虎:如果想要 0 1 0 1,应该怎么改呢?

实习生:难道用range?边说边敲下以下代码。

  • package main
  • import "fmt"
  • func main() {
  • s := []int{0, 1}
  • for _, value := range s {
  • s = append(s, value)
  • }
  • fmt.Printf("s的值是:%+v\n", s)
  • }

胖虎:那你知道为什么吗?

实习生:范围循环在迭代过程中,难道是迭代值的拷贝?我再试下吧。

  • package main
  • import "fmt"
  • func main() {
  • s := []int{0, 1}
  • for _, value := range s {
  • value += 10
  • }
  • fmt.Printf("s的值是:%+v\n", s)
  • }

实习生:跟我猜想的一样。

image-20220420145447719

胖虎点点头:范围遍历在开始遍历数据之前,会先拷贝一份被遍历的数据,所以在遍历过程中去修改被遍历的数据,只是修改拷贝的数据,不会影响到原数据。

而普通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转换一下,而有的编程语言不需要,比如说世界上最好的语言。

image-20220420153836374

当然使用 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

需要注意的是:

  • 和切片不同的是,迭代过程中,删除还未迭代到的键值对,则该键值对不会被迭代。
  • 在迭代过程中,如果创建新的键值对,那么新增键值对,可能被迭代,也可能不会被迭代。
  • 针对 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 main
  • import (
  • "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-8 3552 334038 ns/op
  • BenchmarkRangeIntSlice-8 3544 321965 ns/op

名词解释:

BenchmarkForIntSlice-8,即 GOMAXPROCS,默认等于 CPU 核数。

3552 代表运行了多少次

334038 ns/op 每次执行平均时间,

由此可见 ,遍历 []int 类型的切片,for 与 range 性能两者几乎没有区别。

[]struct

如果是稍微复杂一点的[]struct类型呢?

  • type Item struct {
  • id int
  • 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-8 3373856 359.7 ns/op
  • BenchmarkRangeIndexStruct-8 3587757 329.0 ns/op
  • BenchmarkRangeStruct-8 3478 288161 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-8 734905 1545 ns/op
  • BenchmarkRangePointer-8 709748 1558 ns/op

切片元素从结构体Item替换为指针*Item后,for 和 range 的性能几乎是一样的。而且使用指针还有另一个好处,可以直接修改指针对应的结构体的值。

所以说for 不一定比range快。

实习生:那我总结一下吧。

image-20220420182821297

胖虎:总结的不错,下次不要总结了。

实习生:那我走?

胖虎:哈哈哈哈,开玩笑的

方便获取更多学习、工作、生活信息请关注本站微信公众号城东书院 微信服务号城东书院 微信订阅号
推荐内容
相关内容
栏目更新
栏目热门