在 Go 语言中,引用类型(reference types)是指在赋值或传递参数时,不会复制底层数据,而是传递对数据的引用(即指针)的类型。主要的引用类型包括以下几种:
-
切片(Slice):切片本身是对底层数组的引用,包含指针(指向数组元素)、长度和容量。赋值或传递切片时,复制的是切片结构体(指针、长度、容量),但它们指向同一个底层数组,因此修改切片元素会影响原切片。
// 切片是引用类型
slice1 := []int{1, 2, 3}
slice2 := slice1 // 两个变量引用同一个底层数组
slice2[0] = 100 // 会影响 slice1
fmt.Println(slice1) // [100 2, 3]
-
映射(Map):map 是引用类型,创建 map 时返回的是一个指针(内部实现为指向哈希表的指针)。赋值或传递 map 变量时,传递的是指针的副本,操作的是同一个底层数据结构,修改 map 内容会影响所有引用它的变量。
// 映射是引用类型
map1 := map[string]int{"a": 1, "b": 2}
map2 := map1 // 引用同一个底层数据结构
map2["a"] = 100 // 会影响 map1
fmt.Println(map1) // map[a:100 b:2]
-
通道(Channel):channel 本质上是一个指针类型(指向内部的通道结构体),赋值或传递 channel 时,传递的是指针副本,所有引用指向同一个通道实例,操作会影响同一个通道。
// 通道是引用类型
ch1 := make(chan int, 3)
ch2 := ch1 // 引用同一个通道
go func() {
ch2 <- 42 // 通过 ch2 发送
}()
fmt.Println(<-ch1) // 通过 ch1 接收,输出 42
-
函数(Function):函数在 Go 中是引用类型,函数变量存储的是函数的地址。赋值或传递函数变量时,传递的是地址引用,多个变量可以指向同一个函数。
// 函数也是引用类型
func1 := func(x int) int { return x * 2 }
func2 := func1 // 引用同一个函数
fmt.Println(func2(5)) // 10
-
指针(Pointer):指针显式地存储内存地址。虽然是指针,但它本身是值类型(存储地址的值),但常用于实现引用语义。
// 指针也是引用语义 x := 10 p1 := &x // p1 指向 x p2 := p1 // p2 也指向同一个地址 *p2 = 20 fmt.Println(x) // 20
-
接口(Interface):接口变量在运行时包含类型信息(动态类型)和数据指针(动态值)。当接口存储引用类型时,传递接口变量会复制接口结构体,但内部的指针仍指向原数据,因此修改会影响原数据(注:若接口存储值类型,则行为不同)。
package main
import "fmt"
// 定义一个结构体类型
type Person struct {
Name string
Age int
}
// 为 Person 实现 String() 方法,满足 fmt.Stringer 接口
func (p Person) String() string {
return fmt.Sprintf("%s (%d years old)", p.Name, p.Age)
}
func main() {
// 创建一个 Person 实例
p := Person{Name: "Alice", Age: 30}
// 将 p 赋值给接口变量 i1(接口会存储指向 p 的指针)
var i1 fmt.Stringer = p // 值复制,但接口内部持有指针 → 引用语义
var i2 fmt.Stringer = i1 // i2 和 i1 持有相同的底层数据指针
// 修改原始结构体(注意:p 是值类型,但接口持有的是 &p)
// 实际上这里需要注意:接口存储的是值的副本?等等!
// 关键点来了:Go 接口存储的是(类型,值指针)对
// 所以即使 p 是值,接口持有的是指向原始值的指针(在赋值时取地址)
// 正确演示引用行为:使用指针
p2 := &Person{Name: "Bob", Age: 25}
var i3 fmt.Stringer = p2
var i4 fmt.Stringer = i3
// 修改通过指针指向的数据
p2.Age = 26
// i3 和 i4 看到的是同一个底层数据
fmt.Println("i3:", i3) // i3: Bob (26 years old)
fmt.Println("i4:", i4) // i4: Bob (26 years old)
// 再修改一次
(*p2).Age = 27
fmt.Println("After modify, i3:", i3) // i3: Bob (27 years old)
fmt.Println("After modify, i4:", i4) // i4: Bob (27 years old)
// 证明:接口是引用类型,i3 和 i4 共享同一份数据
}
输出结果:
i3: Bob (26 years old) i4: Bob (26 years old) After modify, i3: Bob (27 years old) After modify, i4: Bob (27 years old)
为什么接口是 引用类型?
Go 接口的底层实现是一个结构体:
type eface struct { // empty interface
_type *_type
data unsafe.Pointer
}
- data 是一个 指针,指向实际的数据。
- 多个接口变量赋值时,复制的是这个结构体(包含指针),不复制底层数据。
- 因此,修改底层数据会影响所有持有相同指针的接口变量。
关键结论:
Go 接口是引用类型: 它不存储数据副本,而是存储 指向数据的指针,多个接口共享同一份数据。
推荐写法(更清晰地体现引用语义):
var i1 fmt.Stringer = &Person{"Charlie", 30}
var i2 = i1
// 修改底层数据会同步反映到 i1 和 i2
这样更直观地体现“引用”行为。
这些类型的特点是:传递或赋值时开销小(仅复制引用),且对其内容的修改会影响所有引用该数据的变量。这与值类型(如 int、struct、array 等)形成对比,值类型在赋值或传递时会复制完整数据。