值类型和引用类型

在 Go 语言中,数据类型可以分为 值类型引用类型。这两种类型的主要区别在于它们在赋值、传递和存储时的行为方式。

值类型 (Value Types)

值类型的变量直接存储其数据值,赋值或传递时会复制整个值。修改副本不会影响原始数据。

常见的值类型

  • 基本数据类型:intfloatboolstring 等。
  • 复合数据类型:arraystruct

注意,string 类型看起来是可变的,实际上底层实现是不可变的字节数组,重新赋值会创建新的字符串对象。

示例

 1package main
 2
 3import "fmt"
 4
 5func main() {
 6    // 值类型示例:int
 7    a := 10
 8    b := a // b 是 a 的副本
 9    b = 20 // 修改 b 不会影响 a
10    fmt.Println(a, b) // 输出: 10 20
11
12    // 值类型示例:array
13    arr1 := [3]int{1, 2, 3}
14    arr2 := arr1 // arr2 是 arr1 的副本
15    arr2[0] = 100 // 修改 arr2 不会影响 arr1
16    fmt.Println(arr1, arr2) // 输出: [1 2 3] [100 2 3]
17}

特点

  • 赋值或传递时,数据会被完整复制。
  • 修改副本不会影响原始数据。
  • 存储空间通常在栈上(除非逃逸到堆)。

引用类型(Reference Types)

引用类型的变量存储的是数据的引用(指针),赋值或传递时只会复制引用,而不会复制底层数据。因此,多个变量可能共享同一份数据,修改其中一个变量会影响其他变量。

常见的引用类型

slicemapchannelinterfacefunctionpointer

示例

 1package main
 2
 3import "fmt"
 4
 5func main() {
 6    // 引用类型示例:slice
 7    s1 := []int{1, 2, 3}
 8    s2 := s1 // s2 和 s1 共享底层数组
 9    s2[0] = 100 // 修改 s2 会影响 s1
10    fmt.Println(s1, s2) // 输出: [100 2 3] [100 2 3]
11
12    // 引用类型示例:map
13    m1 := map[string]int{"a": 1, "b": 2}
14    m2 := m1 // m2 和 m1 共享底层数据
15    m2["a"] = 100 // 修改 m2 会影响 m1
16    fmt.Println(m1, m2) // 输出: map[a:100 b:2] map[a:100 b:2]
17}

特点

  • 赋值或传递时,只复制引用(指针),底层数据不会被复制。
  • 多个变量可能共享同一份数据,修改其中一个会影响其他。
  • 存储空间通常在堆上。

函数的参数传递

Go 仅有值传递,不存在引用传递。

函数如果使用参数,该参数变量称为函数的形参。形参就像定义在函数体内的局部变量

调用函数,可以通过两种方式来传递参数,即值传递引用传递,或者叫作传值和传引用。

值传递

指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数(原内容数据)。Go语言使用的是值传递,即在调用过程中不会影响到原内容数据。每次调用函数,都将实参复制一份再传递到函数中。

引用传递

指在调用函数时将实际参数的地址直接传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。

  • 传指针使得多个函数能操作同一个对象。
  • 传指针更轻量级(8 bytes),只需要传内存地址。如果参数是非指针参数,那么值传递的过程中,每次在复制上面就会花费相对较多的系统开销(内存和时间)。所以要传递大的结构体的时候,用指针是一个明智的选择。

Go 有时看起来是引用传递的原因是Go语言有值类型引用类型,但是它们都是值传递。当参数是引用类型时,Go 按照值传递先复制了一份,即复制了个指针,这样就不会担心复制一份实参造成浪费。也就是说Go可以通过传递引用类型的变量达到和引用传递一样的效果。