Golang的类型系统将变量分为值类型和引用类型两大类,两者的核心差异体现在内存存储和赋值传递的行为上,准确判断变量类型是每个Golang开发者需要掌握的基础能力。

值类型与引用类型的定义
值类型
值类型的变量直接存储对应的值,变量在内存中占据的空间大小就是其存储值的大小。当对值类型变量进行赋值、传参操作时,会复制整个变量的值,生成一个新的独立副本,修改新副本不会影响原始变量。
常见的值类型包括:
- 基本数据类型:int、int8、int16、int32、int64、uint、uint8、uint16、uint32、uint64、float32、float64、complex64、complex128、bool、string
- 复合类型:数组、结构体(struct)
引用类型
引用类型的变量存储的是指向底层数据的指针,变量本身只占一个指针大小的内存空间,实际数据存储在堆区或其他内存区域。当对引用类型变量进行赋值、传参时,复制的是指针,新变量和原始变量会指向同一份底层数据,修改其中任意一个都会影响另一个。
常见的引用类型包括:
- 切片(slice)
- 映射(map)
- 通道(channel)
- 函数(func)
- 指针(*T,虽然指针本身是值类型,但它指向的数据修改会影响所有持有该指针的变量,行为上类似引用)
判断值类型与引用类型的方法
方法一:观察赋值后的修改影响
这是最直观的判断方式,创建变量后复制一份,修改副本的内容,观察原始变量是否发生变化,如果原始变量变化则为引用类型,否则为值类型。
值类型示例:
package main
import "fmt"
func main() {
// 定义int类型变量,属于值类型
var a int = 10
b := a // 赋值操作,复制a的值给b
b = 20 // 修改b的值
fmt.Println("a的值:", a) // 输出a的值: 10,原始变量未被修改
fmt.Println("b的值:", b) // 输出b的值: 20
}
引用类型示例:
package main
import "fmt"
func main() {
// 定义切片变量,属于引用类型
s1 := []int{1, 2, 3}
s2 := s1 // 赋值操作,复制的是切片底层的指针
s2[0] = 100 // 修改s2的元素
fmt.Println("s1的值:", s1) // 输出s1的值: [100 2 3],原始变量被修改
fmt.Println("s2的值:", s2) // 输出s2的值: [100 2 3]
}
方法二:查看类型的零值表现
值类型的零值是其对应类型的默认空值,比如int的零值是0,bool的零值是false;引用类型的零值统一是nil,因为指针未指向任何有效数据。
可以通过声明变量不初始化,打印其零值来判断:
package main
import "fmt"
func main() {
var i int // 值类型,零值是0
var s []int // 引用类型,零值是nil
var m map[string]int // 引用类型,零值是nil
var st struct{} // 值类型,零值是空结构体
fmt.Printf("int零值: %v, 类型: %Tn", i, i)
fmt.Printf("slice零值: %v, 类型: %Tn", s, s)
fmt.Printf("map零值: %v, 类型: %Tn", m, m)
fmt.Printf("struct零值: %v, 类型: %Tn", st, st)
}
方法三:通过内存地址对比
值类型赋值后,两个变量的内存地址不同;引用类型赋值后,两个变量的指针指向的底层数据地址相同。
package main
import "fmt"
func main() {
// 值类型数组
arr1 := [3]int{1,2,3}
arr2 := arr1
fmt.Printf("arr1地址: %p, arr2地址: %pn", &arr1, &arr2) // 两个地址不同
// 引用类型切片
sl1 := []int{1,2,3}
sl2 := sl1
fmt.Printf("sl1底层数据地址: %p, sl2底层数据地址: %pn", sl1, sl2) // 两个地址相同
}
方法四:参考官方类型分类
Golang官方文档中明确划分了类型的归属,对于不确定的类型可以直接查阅官方类型说明,这是最准确的判断依据。需要注意的是string类型比较特殊,它底层是不可变的字节序列,赋值时会复制底层的指针和长度,但由于内容不可变,表现上类似值类型,官方将其归类为值类型。
特殊类型说明
指针类型本身属于值类型,因为指针变量存储的是地址值,赋值时会复制地址值,但通过这个地址可以修改指向的数据。比如*int类型的变量,赋值后两个指针变量是不同的,但它们指向同一个int变量的地址,修改其中一个指针指向的内容会影响另一个。
package main
import "fmt"
func main() {
a := 10
p1 := &a
p2 := p1 // 复制指针值
*p2 = 20
fmt.Println("a的值:", a) // 输出20,修改p2指向的内容影响了a
fmt.Printf("p1地址: %p, p2地址: %pn", &p1, &p2) // p1和p2本身的地址不同
}