值类型和引用类型
在 Go 语言中,数据类型可以分为 值类型 和 引用类型。这两种类型的主要区别在于它们在赋值、传递和存储时的行为方式。
值类型 (Value Types)
值类型的变量直接存储其数据值,赋值或传递时会复制整个值。修改副本不会影响原始数据。
常见的值类型
- 基本数据类型:
int
、float
、bool
、string
等。 - 复合数据类型:
array
、struct
。
注意,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)
引用类型的变量存储的是数据的引用(指针),赋值或传递时只会复制引用,而不会复制底层数据。因此,多个变量可能共享同一份数据,修改其中一个变量会影响其他变量。
常见的引用类型
slice
、map
、channel
、interface
、function
、pointer
。
示例
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可以通过传递引用类型的变量达到和引用传递一样的效果。