Go语言中常见100问题-#30 for range陷阱与解决方法

前言

Go语言中可以通过for range很方便地遍历数据结构中的元素,开发者不用再处理索引以及索引最终的值。但是在使用for range时存在一些坑,需要我们谨慎小心。下面通过具体的例子进行说明。

for range 使用方法

for range 迭代支持的数据结构有以下几种:

  • 字符串
  • 数组
  • 指向数组的指针
  • 切片
  • map
  • 通道

相比普通的for循环,range循环要简洁不少,更不容易出错。我们不必处理条件表达式和循环变量,下面是for range作用于字符串上的示例程序。

golang 复制代码
s := []string{"a", "b", "c"}
for i, v := range s {
    fmt.Printf("index=%d, value=%s\n", i, v)
}

上述程序会遍历切片中的每一个元素,在每次迭代的时候,range返回一对索引和元素值,赋值给变量i和v. 通常来说,range会返回两个值,但当它作用于通道上时,只会返回一个值(元素值),不会返回索引。有时候,我们只对元素值感兴趣,并不关心索引值。可以将索引值赋值给 _ ,因为在Go语言中存在未使用的变量将会报错。示例程序如下:

golang 复制代码
s := []string{"a", "b", "c"}
for _, v := range s {
    fmt.Printf("value=%s\n", v)
}

如果不关心元素值,可以直接忽略第二个元素。像下面这样编写代码。

golang 复制代码
for i := range s {}

案例引入

理解每次迭代中值是如何处理的是有效使用for range的关键。下面通过具体的例子说明, 我们将创建一个 account 结构体,它包含一个 balance 字段。

golang 复制代码
type account struct {
    balance float32
}

接下来,创建一个装载account元素的切片,然后使用for range对切片进行迭代。在迭代的过程中,增加balance的值。

golang 复制代码
accounts := []account{
    {balance: 100.},
    {balance: 200.},
    {balance: 300.},
}

for _, a := range accounts {
    a.balance += 1000
}

执行上述代码后,猜测现在accounts中元素值是下面哪个?

console 复制代码
[{100} {200} {300}]
[{1100} {1200} {1300}]

正确答案是第一个,for range循环没有改变切片中元素值,为啥呢?

原因分析

在Go语言中,每次赋值都是值拷贝。

  • 如果函数的返回值是一个结构体,将它赋值给一个变量,会对返回值的结构体进行值拷贝。
  • 如果函数的返回值是一个指针,将它赋值给一个变量,会对返回的地址值进行拷贝(在64位系统上是64bit值)。

牢记上述规则可以避免共性问题。当range迭代循环数据结构时,将会对遍历到的元素值进行拷贝。回到上面的例子,在对accounts进行遍历时,变量a的值是对原始值的拷贝,所以再增加a.balance的值并不会改变切片accounts中的元素值。

解决方法

如果我们想修改切片中的元素值,主要有两种方法。方法一通过索引访问切片中的元素,具体可以通过for range或普通的for循环实现。示例程序如下。

golang 复制代码
for i := range accounts {
    accounts[i].balance += 1000
}

for i := 0; i < len(accounts); i++ {
    accounts[i].balance += 1000
}

for range和普通的for循环,哪种方法更好呢?需要具体情况具体分析,如果需要遍历全部元素,for range更简洁些,如果对要更新的元素有要求,比如只更新偶数位置上的元素,应采用普通的for循环。

方法二是将切片中的元素定义为指针类型,像下面这样,此时a的值是切片中元素的地址拷贝,所以修改a的值会更新切片中的元素。但是,这种方法有两个缺点,一是需要修改切片中的元素类型,在某些场合下,我们是没法修改的。二是在对性能要求很高的环境中,迭代切片中的指针对CPU来说可能更低效,因为缺少可预测性。

golang 复制代码
accounts := []*account{
    {balance: 100.},
    {balance: 200.},
    {balance: 300.},
}

for _, a := range accounts {
    a.balance += 1000
}

总结,我们应该记住,for range中返回的值是原始值的拷贝。对返回值进行修改通常不会影响切片中的元素值,除非将切片中的元素类型改为指针类型,或是通过索引遍历更新元素。

相关推荐
初晴~21 分钟前
【Redis分布式锁】高并发场景下秒杀业务的实现思路(集群模式)
java·数据库·redis·分布式·后端·spring·
盖世英雄酱5813626 分钟前
InnoDB 的页分裂和页合并
数据库·后端
滚雪球~38 分钟前
npm error code ETIMEDOUT
前端·npm·node.js
沙漏无语39 分钟前
npm : 无法加载文件 D:\Nodejs\node_global\npm.ps1,因为在此系统上禁止运行脚本
前端·npm·node.js
supermapsupport41 分钟前
iClient3D for Cesium在Vue中快速实现场景卷帘
前端·vue.js·3d·cesium·supermap
brrdg_sefg42 分钟前
WEB 漏洞 - 文件包含漏洞深度解析
前端·网络·安全
小_太_阳1 小时前
Scala_【2】变量和数据类型
开发语言·后端·scala·intellij-idea
胡西风_foxww1 小时前
【es6复习笔记】rest参数(7)
前端·笔记·es6·参数·rest
直裾1 小时前
scala借阅图书保存记录(三)
开发语言·后端·scala
m0_748254881 小时前
vue+elementui实现下拉表格多选+搜索+分页+回显+全选2.0
前端·vue.js·elementui