切片是否按值传递?

Are slices passed by value?

提问人:duck 提问时间:10/12/2016 最后编辑:Jonathan Hallduck 更新时间:10/18/2022 访问量:89238

问:

在 Go 中,我试图为我的旅行推销员问题制作一个加扰切片函数。在执行此操作时,我注意到当我开始编辑切片时,每次我传入它时,我给的加扰函数都不同。

经过一些调试,我发现这是由于我在函数中编辑了切片。但是,既然 Go 应该是一种“按值传递”的语言,这怎么可能呢?

https://play.golang.org/p/mMivoH0TuV

我提供了一个游乐场链接来说明我的意思。 通过删除第 27 行,您将获得与保留它不同的输出,这应该不会有什么区别,因为该函数在作为参数传入时应该制作自己的切片副本。
有人可以解释这种现象吗?

Go 切片 按值传递

评论


答:

267赞 icza 10/12/2016 #1

Go 中的所有内容都是按值传递的,切片也是如此。但是切片值是一个标头,描述后备数组的连续部分,而切片值仅包含指向实际存储元素的数组的指针。切片值不包括其元素(与数组不同)。

因此,当您将切片传递给函数时,将从此标头(包括指针)进行复制,该指针将指向相同的后备数组。修改切片的元素意味着修改后备数组的元素,因此共享同一后备数组的所有切片都将“观察”更改。

若要查看切片标头中的内容,请查看反射。SliceHeader 类型:

type SliceHeader struct {
    Data uintptr
    Len  int
    Cap  int
}

请参阅相关/可能的重复问题:函数切片参数与全局变量的性能?

阅读博客文章:Go Slices:用法和内部结构

请注意,当您将切片传递给函数时,如果该函数修改了切片的“现有”元素,调用方将看到/观察更改。如果函数向切片添加新元素,则需要更改切片标头(最小长度,但也可能涉及分配新的后备数组),调用方将看不到(并非不返回新的切片标头)。

地图则不然,因为地图是后台的指针,如果将地图传递给函数,并且该函数向地图添加新条目,则地图指针不会更改,因此调用方将看到更改后的地图(新条目),而不会在更改后返回地图。

此外,关于切片和映射,请参阅 Go 中的映射初始化以及为什么切片值有时会过时,但永远不会映射值?

评论

2赞 icza 10/12/2016
@user4901806 如果您不想修改传递的切片的元素(它指向的后备数组的元素),那么是的,请复制。
24赞 icza 6/17/2017
@Sahas 切片标头包含长度。如果追加一个元素,则必须增加长度,因此即使后备数组有空间容纳此附加元素,并且没有分配新的后备数组并复制现有元素,原始切片标头也不会“看到”它。这就是内置函数必须返回新切片值的原因。更不用说是否必须分配一个新阵列了......append()
1赞 icza 5/22/2019
@vas不是嵌入,它只是一个常规字段。如果只有一个字段,那么答案是肯定的。如果有多个字段,则可能会应用隐式填充,因此整体结构大小可能会更大。type Container struct {data []byte}
1赞 icza 5/23/2019
@vas 是的,无论您使用嵌入字段还是命名字段,它们都使用相同的内存量。
1赞 Josh Hibschman 5/13/2021
它可能会丰富答案,以澄清“一切都按价值传递”。切片和映射都是按值传递的,但称为“引用类型”(blog.golang.org/maps)。更令人困惑的是,它们的引用类型行为存在差异。例如,将一个切片从 func A 传递给 func B,向切片添加一个值,然后 func A 将看不到添加的值。但是将一个映射从函数 A 传递到函数 B 中,添加到函数 B 中的映射中,然后函数 A 看到附加值。去玩 play.golang.org/p/o9gm7JtDbMm
5赞 Hasan A Yousef 6/6/2020 #2

切片在传递时与指向基础数组的指针一起传递,因此切片是指向基础数组的小结构。小结构被复制,但它仍然指向相同的基础数组。包含切片元素的内存块通过“引用”传递。包含容量、元素数量和指向元素的指针的切片信息三元组按值传递。

处理传递给函数的切片的最佳方法(如果切片的元素作到函数中,并且我们不希望这反映在元素内存块中,则使用以下命令复制它们:copy(s, *c)

package main

import "fmt"

type Team []Person
type Person struct {
    Name string
    Age  int
}

func main() {
    team := Team{
        Person{"Hasan", 34}, Person{"Karam", 32},
    }
    fmt.Printf("original before clonning: %v\n", team)
    team_cloned := team.Clone()
    fmt.Printf("original after clonning: %v\n", team)
    fmt.Printf("clones slice: %v\n", team_cloned)
}

func (c *Team) Clone() Team {
    var s = make(Team, len(*c))
    copy(s, *c)
    for index, _ := range s {
        s[index].Name = "change name"
    }
    return s
}

但要小心,如果这个切片包含进一步的复制,则需要进一步复制,因为我们仍然会让子切片元素共享指向相同的内存块元素,例如:sub slice

type Inventories []Inventory
type Inventory struct { //instead of: map[string]map[string]Pairs
    Warehouse string
    Item      string
    Batches   Lots
}
type Lots []Lot
type Lot struct {
    Date  time.Time
    Key   string
    Value float64
}

func main() {
ins := Inventory{
        Warehouse: "DMM",
        Item:      "Gloves",
        Batches: Lots{
            Lot{mustTime(time.Parse(custom, "1/7/2020")), "Jan", 50},
            Lot{mustTime(time.Parse(custom, "2/1/2020")), "Feb", 70},
        },
    }

   inv2 := CloneFrom(c Inventories)
}

func (i *Inventories) CloneFrom(c Inventories) {
    inv := new(Inventories)
    for _, v := range c {
        batches := Lots{}
        for _, b := range v.Batches {
            batches = append(batches, Lot{
                Date:  b.Date,
                Key:   b.Key,
                Value: b.Value,
            })
        }

        *inv = append(*inv, Inventory{
            Warehouse: v.Warehouse,
            Item:      v.Item,
            Batches:   batches,
        })
    }
    (*i).ReplaceBy(inv)
}

func (i *Inventories) ReplaceBy(x *Inventories) {
    *i = *x
}
11赞 Arin Yazilim 12/8/2020 #3

您可以在下面找到一个示例。简而言之,切片也按值传递,但原始切片和复制的切片链接到相同的基础数组。如果其中一个切片发生更改,则基础数组将更改,则其他切片也会更改。

package main

import "fmt"

func main() {
    x := []int{1, 10, 100, 1000}
    double(x)
    fmt.Println(x) // ----> 3 will print [2, 20, 200, 2000] (original slice changed)
}

func double(y []int) {
    fmt.Println(y) // ----> 1 will print [1, 10, 100, 1000]
    for i := 0; i < len(y); i++ {
        y[i] *= 2
    }
    fmt.Println(y) // ----> 2 will print [2, 20, 200, 2000] (copy slice + under array changed)
}
1赞 Al Elizalde 4/24/2021 #4

为了补充这篇文章,这里有一个通过引用传递你分享的 Golang PlayGround 的例子:

type point struct {
    x int
    y int
}

func main() {
    data := []point{{1, 2}, {3, 4}, {5, 6}, {7, 8}}
    makeRandomDatas(&data)
}

func makeRandomDatas(dataPoints *[]point) {
    for i := 0; i < 10; i++ {
        if len(*dataPoints) > 0 {
            fmt.Println(makeRandomData(dataPoints))
        } else {
            fmt.Println("no more elements")
        }
    }

}

func makeRandomData(cities *[]point) []point {
    solution := []point{(*cities)[0]}                 //create a new slice with the first item from the old slice
    *cities = append((*cities)[:0], (*cities)[1:]...) //remove the first item from the old slice
    return solution

}
6赞 rams time 9/6/2022 #5

Slice 将使用按值传递给函数,但我们不应该使用 append 来添加函数中的 slice 值,而应该直接使用赋值。原因是 append 将创建新的内存并将值复制到其中。下面是示例。

去游乐场

     // Go program to illustrate how to
        // pass a slice to the function
        package main
        
        import "fmt"
        
        // Function in which slice
        // is passed by value
        func myfun(element []string) {
        
            // Here we only modify the slice
            // Using append function
            // Here, this function only modifies
            // the copy of the slice present in
            // the function not the original slice
            element = append(element, "blackhole")
            fmt.Println("Modified slice: ", element)
        }
        
        func main() {
        
            // Creating a slice
            slc := []string{"rocket", "galaxy", "stars", "milkyway"}
            fmt.Println("Initial slice: ", slc)
            //slice pass by value
            myfun(slc)
            fmt.Println("Final slice: ", slc)
        }
Output-
    Initial slice:  [rocket galaxy stars milkyway]
    Modified slice:  [rocket galaxy stars milkyway blackhole]
    Final slice:  [rocket galaxy stars milkyway]

去游乐场

    // Go program to illustrate how to
        // pass a slice to the function
        package main
        import "fmt"
        
        // Function in which slice
        // is passed by value
        func myfun(element []string) {
        
            // Here we only modify the slice
            // Using append function
            // Here, this function only modifies
            // the copy of the slice present in
            // the function not the original slice
            element[0] = "Spaceship"
            element[4] = "blackhole"
            element[5] = "cosmos"
            fmt.Println("Modified slice: ", element)
        }
        
        func main() {
        
            // Creating a slice
            slc := []string{"rocket", "galaxy", "stars", "milkyway", "", ""}
            fmt.Println("Initial slice: ", slc)
            //slice pass by value
            myfun(slc)
            fmt.Println("Final slice: ", slc)
        }
Output-
    Initial slice:  [rocket galaxy stars milkyway  ]
    Modified slice:  [Spaceship galaxy stars milkyway blackhole cosmos]
    Final slice:  [Spaceship galaxy stars milkyway blackhole cosmos]