Go

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

2023-05-27  本文已影响0人  红薯爱帅

1. 概述

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

2. for-range的数据副本

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

验证代码如下,其中:

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中,数据类型分为不可变对象可变对象

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

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中,对不同数据类型,内存的数据结构不同,有的是值传递,有的是地址传递

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

上一篇下一篇

猜你喜欢

热点阅读