美文网首页Go
Go开发的各种坑 - for-range的数据副本

Go开发的各种坑 - for-range的数据副本

作者: 红薯爱帅 | 来源:发表于2023-05-27 00:21 被阅读0次

1. 概述

本文介绍for-range的一个坑,由于其他语言很少遇到,C++没有range操作,Python没有取地址操作,唯独在golang中均支持,所以容易入坑。
另外,顺带着介绍一下变量赋值操作,作为拓展阅读吧。

2. for-range的数据副本

通过for-range可以遍历arrayslicemapchannel,预声明的迭代变量,是唯一地址的数据副本,既不是指向被迭代对象的每一项,也不随着每个loop即时申请新内存。
所以,在使用for-range遍历可迭代对象时,切不可对迭代变量取地址,因为取到的地址是不变的。

验证代码如下,其中:

  • case0和case1是等价的,是错误示例
  • case2和case3均是正确示例
package main

import "log"

type someStruct struct {
    name string
    age  int
}

func doNotGetPointer() {
    var persons = []someStruct{
        {"foo", 1},
        {"bar", 2},
    }

    // case0
    var data0 []any
    for _, item := range persons {
        data0 = append(data0, &item) // 由于item是副本变量,所以地址不变
    }

    // case1
    var data1 []any
    var item1 someStruct
    for i := 0; i < len(persons); i++ {
        item1 = persons[i]
        data1 = append(data1, &item1)
    }

    // case2
    var data2 []any
    var item2 *someStruct
    for i := 0; i < len(persons); i++ {
        item2 = &persons[i]
        data2 = append(data2, item2)
    }

    // case3
    var data3 []any
    for i := 0; i < len(persons); i++ {
        var item3 someStruct
        item3 = persons[i]
        data3 = append(data3, &item3)
    }

    log.Printf("data0 %+v", data0)
    log.Printf("data1 %+v", data1)
    log.Printf("data2 %+v", data2)
    log.Printf("data3 %+v", data3)
}

func main() {
    doNotGetPointer()
}
  • 运行结果
% go run test_for1.go
2023/05/27 23:36:27 data0 [0x1400000c030 0x1400000c030]
2023/05/27 23:36:27 data1 [0x1400000c048 0x1400000c048]
2023/05/27 23:36:27 data2 [0x14000074180 0x14000074198]
2023/05/27 23:36:27 data3 [0x1400000c060 0x1400000c078]

3. 变量赋值

在不同的编程语言中,数据类型大体可分为基本数据类型引用数据类型
对于引用数据类型,常常也是容器类型,其值往往是可以动态改变,且赋值给新变量时,只是赋地址,对应的value是一份。
因此,也有了浅copy深copy之说。

3.1. Python

Python中,数据类型分为不可变对象可变对象

  • 不可变对象,例如int, float, str, bool, tuple
  • 可变对象,dict, list, set

注意,可变,是其指向的内存中的值可变。

a = [dict(name=f"name-{i}") for i in range(2)]
print("a", a)

b = [i for i in a]
print("b", b)

b[1]["name"] = "xxx"
print("a", a)
print("b", b)
$ python test.py 
a [{'name': 'name-0'}, {'name': 'name-1'}]
b [{'name': 'name-0'}, {'name': 'name-1'}]
a [{'name': 'name-0'}, {'name': 'xxx'}]   # ===> 修改了b[1]["name"],也对应修改了a[1]的value
b [{'name': 'name-0'}, {'name': 'xxx'}]

3.2. Golang

Go中,对不同数据类型,内存的数据结构不同,有的是值传递,有的是地址传递

  • string、slice、map这三种类型的实现机制类似指针。直接传递,就是地址传递,而不是值传递。(string比较特殊,静态字符串默认是在栈空间上,且只读
  • 其他类型,例如struct等,需要取地址后传递指针。否则,直接传递,就是值传递。
package main

import (
    "fmt"
    "log"
)

func changeName(names []string) {
    for i, _ := range names {
        names[i] = fmt.Sprintf("name-%d", i)
    }
}

func testSlice() {
    var persons = []string{"foo", "bar"}
    log.Printf("%+v", persons)
    changeName(persons)
    log.Printf("%+v", persons)
}

func main() {
    testSlice()
}
% go run test_for1.go
2023/05/28 00:05:07 [foo bar]
2023/05/28 00:05:07 [name-0 name-1]

需要注意的是,对于slice而言,直接传递,虽然可以改变其value,但是最好不要增加item。
因为一旦触发其扩容,则slice的起始地址会发生改变。
如果要改变slice内元素个数,需要传递slice指针,例如names *[]string

相关文章

  • go for-range 的坑

    关键字range可用于循环,类似迭代器操作,它可以遍历slice,array,string,map和channel...

  • 五. Go(Go protobuf)

    gopath开发模式和go modules开发模式对比 goland创建项目时没用go mod模式选项的坑 在Go...

  • BLE开发的各种坑

    这段时间在做低功耗蓝牙(BLE)应用的开发(并不涉及蓝牙协议栈)。总体感觉 Android BLE 还是不太稳定,...

  • C++ Primer: String, Vector and A

    1. String 处理字符串 cctype 处理每个字符 for-range 语句 for-range 改变字符...

  • [转载]BLE开发的各种坑

    这段时间在做低功耗蓝牙(BLE)应用的开发(并不涉及蓝牙协议栈)。总体感觉 Android BLE 还是不太稳定,...

  • MacOS开发遇到的各种坑

    问题1:配置完项目,添加按钮发现无法点击解决方式:AppDelegate中必须把界面类初始化为全局变量(无数只草泥...

  • Go-开发辅助工具

    Golang开发工具 JSON-to-Go JSON-to-Go 是一个将 json 数据转换为 Go 结构体的在...

  • 《go web 编程》第四章 访问数据库:database/sq

    Go 与 PHP 不同的地方是 Go 官方没有提供数据库驱动,而是为开发数据库驱动定义了一些标准接口,开发者可以根...

  • 指针、引用还是传值

    Go 默认使用按值传递来传递参数,也就是传递参数的副本。函数接收参数副本之后,在使用变量的过程中可能对副本的值进行...

  • Go踩过的各种小坑总结

网友评论

    本文标题:Go开发的各种坑 - for-range的数据副本

    本文链接:https://www.haomeiwen.com/subject/ozgqedtx.html