
在go语言中,初学者常会将数组和切片混淆,尤其是在处理集合类型数据时。这两种数据结构虽然都用于存储一系列同类型元素,但其底层实现和行为特性却大相径庭。核心在于:数组是值类型,而切片是引用类型。
数组是Go语言中固定长度的序列。一旦声明,其长度就不可改变。数组是值类型,这意味着当你将一个数组赋值给另一个数组变量,或者将数组作为参数传递给函数时,Go语言会创建一个该数组的完整副本。
数组的声明与初始化:
// 声明一个包含3个整数的数组,并初始化
arr1 := [3]int{1, 2, 3}
fmt.Printf("arr1 类型: %T, 值: %v\n", arr1, arr1) // 输出: arr1 类型: [3]int, 值: [1 2 3]
// 声明时让编译器自动推断数组长度
arr2 := [...]int{1, 2, 3}
fmt.Printf("arr2 类型: %T, 值: %v\n", arr2, arr2) // 输出: arr2 类型: [3]int, 值: [1 2 3]
// 声明一个零值数组,所有元素初始化为对应类型的零值
var arr3 [3]int
fmt.Printf("arr3 类型: %T, 值: %v\n", arr3, arr3) // 输出: arr3 类型: [3]int, 值: [0 0 0]数组的赋值与传递行为:
由于数组是值类型,赋值操作会复制所有元素。
立即学习“go语言免费学习笔记(深入)”;
func modifyArray(a [3]int) {
a[0] = 99 // 修改的是副本
fmt.Println("函数内数组副本:", a)
}
func main() {
myArray := [3]int{10, 20, 30}
anotherArray := myArray // 完整复制 myArray 的所有元素到 anotherArray
fmt.Println("原始数组:", myArray)
fmt.Println("复制后的数组:", anotherArray)
anotherArray[0] = 50 // 修改 anotherArray 不会影响 myArray
fmt.Println("修改 anotherArray 后,原始数组:", myArray)
fmt.Println("修改 anotherArray 后,复制后的数组:", anotherArray)
modifyArray(myArray) // 传递的是 myArray 的副本
fmt.Println("调用函数后,原始数组:", myArray) // myArray 保持不变
}输出:
原始数组: [10 20 30] 复制后的数组: [10 20 30] 修改 anotherArray 后,原始数组: [10 20 30] 修改 anotherArray 后,复制后的数组: [50 20 30] 函数内数组副本: [99 20 30] 调用函数后,原始数组: [10 20 30]
从输出可以看出,对 anotherArray 的修改或在 modifyArray 函数内部对参数 a 的修改,都不会影响 myArray 的内容,因为它们操作的都是独立的副本。
切片是Go语言中最常用的序列类型,它提供了对底层数组的动态视图。切片本身是一个轻量级的数据结构,包含三个组成部分:
切片是引用类型。这意味着当你将一个切片赋值给另一个切片变量,或者将切片作为参数传递给函数时,Go语言仅仅复制了切片头信息(指针、长度、容量),而不会复制底层数组。因此,多个切片变量可能指向同一个底层数组,对其中任何一个切片内容的修改,都会反映在所有指向该底层数组的切片上。
切片的声明与初始化:
// 通过字面量声明并初始化切片
slice1 := []int{1, 2, 3}
fmt.Printf("slice1 类型: %T, 值: %v, 长度: %d, 容量: %d\n", slice1, slice1, len(slice1), cap(slice1))
// 输出: slice1 类型: []int, 值: [1 2 3], 长度: 3, 容量: 3
// 使用 make 函数创建切片:指定长度
slice2 := make([]int, 3) // 创建一个长度为3的切片,元素为零值 [0 0 0]
fmt.Printf("slice2 类型: %T, 值: %v, 长度: %d, 容量: %d\n", slice2, slice2, len(slice2), cap(slice2))
// 输出: slice2 类型: []int, 值: [0 0 0], 长度: 3, 容量: 3
// 使用 make 函数创建切片:指定长度和容量
slice3 := make([]int, 3, 5) // 创建一个长度为3,容量为5的切片
fmt.Printf("slice3 类型: %T, 值: %v, 长度: %d, 容量: %d\n", slice3, slice3, len(slice3), cap(slice3))
// 输出: slice3 类型: []int, 值: [0 0 0], 长度: 3, 容量: 5切片的赋值与传递行为:
切片赋值仅仅复制引用,而不是底层数据。
func modifySlice(s []int) {
s[0] = 999 // 修改的是底层数组
fmt.Println("函数内切片:", s)
}
func main() {
mySlice := []int{100, 200, 300, 400, 500}
anotherSlice := mySlice // 复制 mySlice 的引用,它们现在指向同一个底层数组
fmt.Println("原始切片:", mySlice)
fmt.Println("复制引用的切片:", anotherSlice)
anotherSlice[0] = 555 // 修改 anotherSlice 会影响 mySlice
fmt.Println("修改 anotherSlice 后,原始切片:", mySlice)
fmt.Println("修改 anotherSlice 后,复制引用的切片:", anotherSlice)
modifySlice(mySlice) // 传递的是 mySlice 的引用
fmt.Println("调用函数后,原始切片:", mySlice) // mySlice 的内容已改变
}输出:
原始切片: [100 200 300 400 500] 复制引用的切片: [100 200 300 400 500] 修改 anotherSlice 后,原始切片: [555 200 300 400 500] 修改 anotherSlice 后,复制引用的切片: [555 200 300 400 500] 函数内切片: [999 200 300 400 500] 调用函数后,原始切片: [999 200 300 400 500]
从输出可以看出,对 anotherSlice 的修改或在 modifySlice 函数内部对参数 s 的修改,都会同步影响 mySlice,因为它们都操作的是同一个底层数组。
现在,让我们回到最初的问题代码,分析其行为:
package main
import (
"fmt"
"math/rand" // 推荐使用 math/rand
"time"
)
// shuffle 函数接受一个切片,并对其进行原地洗牌
func shuffle(arr []int) {
// 使用当前时间戳作为随机数种子,确保每次运行结果不同
r := rand.New(rand.NewSource(time.Now().UnixNano()))
for i := len(arr) - 1; i > 0; i-- {
// 从 [0, i] 范围内选择一个随机索引 j
j := r.Intn(i + 1)
// 交换 arr[i] 和 arr[j]
arr[i], arr[j] = arr[j], arr[i]
}
}
func main() {
// 1. 声明并初始化一个切片,而不是数组
// 这行代码创建了一个匿名的底层数组 {1, 2, 3, 4, 5},
// 并让 arr 成为指向该底层数组的切片。
arr := []int{1, 2, 3, 4, 5}
// 2. arr2 接收 arr 的引用
// 这行代码并没有复制底层数组,而是复制了 arr 的切片头信息。
// 此时,arr 和 arr2 都指向同一个底层数组。
arr2 := arr
fmt.Println("原始切片 arr:", arr)
fmt.Println("原始切片 arr2 (与 arr 共享底层数组):", arr2)
// 3. 调用 shuffle 函数,传入 arr 的引用
// shuffle 函数接收的是 arr 的切片头信息副本,但这个副本仍然指向与 arr 相同的底层数组。
// 因此,函数内部对切片内容的修改(洗牌操作)会直接修改这个共享的底层数组。
shuffle(arr)
// 4. 打印 arr2 的内容
// 由于 arr2 与 arr 共享同一个底层数组,且底层数组已被 shuffle 函数修改,
// 所以 arr2 的内容也会同步发生变化,显示洗牌后的结果。
fmt.Println("洗牌后 arr:", arr)
fmt.Println("洗牌后 arr2 (与 arr 共享底层数组,内容已同步改变):")
for _, i := range arr2 {
fmt.Printf("%d ", i)
}
fmt.Println()
}代码行为解释:
sourceSlice := []int{1, 2, 3}
destSlice := make([]int, len(sourceSlice)) // 创建一个新切片,长度与源切片相同
copy(destSlice, sourceSlice) // 将源切片的内容复制到新切片
fmt.Println("源切片:", sourceSlice)
fmt.Println("复制切片:", destSlice)
destSlice[0] = 99
fmt.Println("修改复制切片后,源切片:", sourceSlice) // 源切片不受影响
fmt.Println("修改复制切片后,复制切片:", destSlice)Go语言中的数组和切片是处理序列数据的重要工具。理解它们作为值类型和引用类型的本质差异,以及这种差异如何影响赋值和函数参数传递,是编写健壮、可预测Go程序的基石。切片因其灵活性和对底层数组的引用特性而更常用于动态数据处理,但这也要求开发者时刻注意其共享底层数据的行为,避免意外的副作用。
以上就是深入理解Go语言中的数组与切片:值类型与引用类型的行为差异的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号