Golang中数组是值传递还是引用传递?

Golang中数组是值传递还是引用传递? 我有以下代码:

package main

import "fmt"

func main() {
	arr := []string{"hi", "bye"}
	fmt.Printf("arr: %v\n", arr)
	check(arr)
	fmt.Printf("arr: %v\n", arr)
}

func check(c []string) {
	for index, _ := range c {
		c[index] = "ok"
	}
}

它给出了以下输出:

arr: [hi bye]
arr: [ok ok]

这意味着数组是作为引用传递的,而不是作为值传递的,我的理解正确吗?是否有关于此的官方文档?

在 Go 中,除了数组之外,还有其他东西是作为引用传递的吗?


更多关于Golang中数组是值传递还是引用传递?的实战教程也可以访问 https://www.itying.com/category-94-b0.html

7 回复

你好。 在这两段代码中,传递给函数的原始切片的值都被改变了!

更多关于Golang中数组是值传递还是引用传递?的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


Go语言默认是值传递。 不存在像C++那样的引用传递。

切片在传递时,会传递指向底层数组的指针。

func main() {
    fmt.Println("hello world")
}

切片是按值传递的。当你在 check 函数中追加新值时,它不会反映在 main 函数中。 请查看这里:https://play.golang.org/p/cTB7C6ws7Uo

当你将切片的地址传递给 check 函数时,它会在 main 函数中反映出来。 请查看这里:https://play.golang.org/p/x50H9C7Rw2X

您定义的 arr 不是一个数组,而是一个切片。数组看起来应该是这样的:

arr := [2]string{"hi", "bye"}

这意味着,一旦你实例化了数组 arr,你就不能改变它的大小。你可以改变数组元素的值,但不能增加或减少数组中的元素数量。切片则允许你增加和减少元素数量、复制切片等。切片是对底层数组的一个引用。在 Go 语言中,数组不常用,但切片很常用。

显式声明的函数参数总是按值传递。数组、切片、结构体等都是按值传递的。实现“按引用传递”的方式是传递一个指针(指针本身仍然是按值传递的)。然后你可以对该指针进行解引用。

切片虽然也是按值传递,但它内部包装了一个指向底层数组的指针。因此,即使函数是按值传递切片,由于可以通过该指针修改切片,其行为看起来就像是“按引用传递”一样。

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

感谢各位的回复,在你们的支持和基于此进行的额外搜索后,我找到了答案:

切片在传递时,会传递指向底层数组的指针。切片值仅包含一个指向实际存储元素的数组的指针。切片值不包含其元素(这与数组不同)。切片值是一个头部,描述了后备数组的一个连续部分。当你将切片传递给函数时,会复制这个头部(包括指针),该指针将指向同一个后备数组。修改切片的元素意味着修改后备数组的元素,因此所有共享同一后备数组的切片都会“观察到”这个变化。 切片是一个指向底层数组的小型结构体。这个小型结构体会被复制,但它仍然指向同一个底层数组。包含切片元素的内存块是通过“引用”传递的。而保存容量、元素数量和指向元素的指针的切片信息三元组则是通过值传递的。

处理传递给函数的切片的最佳方式(如果函数内会操作切片的元素,而我们不希望这种操作反映到元素内存块上)是使用 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
}

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

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
}

在Go语言中,你提到的[]string实际上是一个切片(slice),而不是数组(array)。这是一个重要的区别。

切片是引用类型,它包含一个指向底层数组的指针、长度和容量。当切片作为参数传递时,虽然切片描述符本身是按值传递的,但由于它包含指向底层数组的指针,所以对切片元素的修改会影响原始数据。

数组是值类型,当数组作为参数传递时,会创建整个数组的副本。让我们通过示例来对比:

package main

import "fmt"

func main() {
    // 示例1:切片(你的代码)
    slice := []string{"hi", "bye"}
    fmt.Printf("原始切片: %v\n", slice)
    modifySlice(slice)
    fmt.Printf("修改后切片: %v\n", slice)
    
    // 示例2:数组
    array := [2]string{"hi", "bye"}
    fmt.Printf("\n原始数组: %v\n", array)
    modifyArray(array)
    fmt.Printf("修改后数组: %v\n", array)
    
    // 示例3:数组指针
    array2 := [2]string{"hi", "bye"}
    fmt.Printf("\n原始数组2: %v\n", array2)
    modifyArrayByPointer(&array2)
    fmt.Printf("修改后数组2: %v\n", array2)
}

func modifySlice(s []string) {
    for i := range s {
        s[i] = "ok"
    }
}

func modifyArray(a [2]string) {
    for i := range a {
        a[i] = "ok"
    }
}

func modifyArrayByPointer(a *[2]string) {
    for i := range a {
        a[i] = "ok"
    }
}

输出:

原始切片: [hi bye]
修改后切片: [ok ok]

原始数组: [hi bye]
修改后数组: [hi bye]

原始数组2: [hi bye]
修改后数组2: [ok ok]

官方文档参考

Go语言中的引用类型

  1. 切片(slice) - 包含指向底层数组的指针
  2. 映射(map) - 引用类型,传递的是描述符的副本
  3. 通道(channel) - 引用类型
  4. 函数(function) - 可以作为值传递
  5. 接口(interface) - 包含类型信息和值指针

值类型

  1. 数组(array) - 完整复制
  2. 结构体(struct) - 默认值传递,除非使用指针
  3. 基本类型 (int, float, bool, string等) - 值传递
package main

import "fmt"

func main() {
    // 映射示例
    m := map[string]int{"a": 1, "b": 2}
    fmt.Printf("原始map: %v\n", m)
    modifyMap(m)
    fmt.Printf("修改后map: %v\n", m)
    
    // 通道示例
    ch := make(chan int, 2)
    ch <- 1
    ch <- 2
    modifyChannel(ch)
    fmt.Printf("通道内容: %d, %d\n", <-ch, <-ch)
}

func modifyMap(m map[string]int) {
    m["a"] = 100
    m["c"] = 3
}

func modifyChannel(ch chan int) {
    // 通道是引用类型,传递的是通道的引用
}

关键点:在Go中,所有参数传递都是按值传递,但对于切片、映射、通道这些引用类型,传递的是它们描述符(包含指针)的副本,因此可以修改底层数据。

回到顶部