0

0

深入理解Go语言中的数组与切片:值类型与引用类型的行为差异

聖光之護

聖光之護

发布时间:2025-07-20 14:22:24

|

968人浏览过

|

来源于php中文网

原创

深入理解Go语言中的数组与切片:值类型与引用类型的行为差异

本文旨在深入解析Go语言中数组(Arrays)与切片(Slices)的核心区别,特别是它们在赋值和函数参数传递时的行为差异。Go语言中的数组是值类型,赋值和传递时会进行完整复制;而切片是引用类型,本质上是对底层数组的引用,赋值和传递时仅复制引用本身,导致对切片内容的修改会影响所有指向同一底层数组的切片。理解这一机制对于编写高效且无意外行为的Go程序至关重要。

go语言中,初学者常会将数组和切片混淆,尤其是在处理集合类型数据时。这两种数据结构虽然都用于存储一系列同类型元素,但其底层实现和行为特性却大相径庭。核心在于:数组是值类型,而切片是引用类型。

Go语言中的数组(Arrays)

数组是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语言中的切片(Slices)

切片是Go语言中最常用的序列类型,它提供了对底层数组的动态视图。切片本身是一个轻量级的数据结构,包含三个组成部分:

  1. 指向底层数组的指针 (Pointer):指向切片第一个元素在底层数组中的位置。
  2. 长度 (Length):切片中当前元素的数量。
  3. 容量 (Capacity):从切片起点到底层数组末尾的元素数量。

切片是引用类型。这意味着当你将一个切片赋值给另一个切片变量,或者将切片作为参数传递给函数时,Go语言仅仅复制了切片头信息(指针、长度、容量),而不会复制底层数组。因此,多个切片变量可能指向同一个底层数组,对其中任何一个切片内容的修改,都会反映在所有指向该底层数组的切片上。

VanceAI Image Resizer
VanceAI Image Resizer

VanceAI推出的在线图片尺寸调整工具

下载

切片的声明与初始化:

// 通过字面量声明并初始化切片
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() 
}

代码行为解释:

  1. arr := []int{1, 2, 3, 4, 5}:这里创建的是一个切片,而不是一个固定大小的数组。Go语言中的 []int 语法表示切片类型。
  2. arr2 := arr:这个赋值操作复制的是 arr 切片的引用(即指向底层数组的指针、长度和容量)。此时,arr 和 arr2 都指向内存中同一个 [1 2 3 4 5] 的底层数组。
  3. shuffle(arr):当 arr 作为参数传递给 shuffle 函数时,传递的也是切片的引用副本。shuffle 函数内部对 arr(实际上是切片头信息副本)的修改,会直接作用于这个共享的底层数组。
  4. 因此,当 shuffle 函数执行完毕后,底层数组已经被洗牌。由于 arr2 仍然指向这个被洗牌后的底层数组,所以打印 arr2 时,会看到洗牌后的结果。

注意事项与最佳实践

  • 区分数组与切片: 明确 [N]Type 是数组,[]Type 是切片。
  • 切片是引用类型: 这是理解切片行为的关键。除非你明确复制底层数据,否则多个切片变量很可能共享同一个底层数组。
  • 复制切片: 如果你需要一个完全独立的切片副本,而不是共享底层数组,可以使用 copy() 函数:
    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)
  • 切片操作与底层数组: append 操作在切片容量不足时,可能会导致底层数组的重新分配,从而使新的切片不再与原切片共享底层数组。
  • 函数参数选择: 如果函数需要修改传入的集合数据,通常应使用切片作为参数。如果希望函数不修改原始数据,且数据量不大,可以考虑使用数组(会复制),或者传入切片的副本(通过 copy() 创建)。

总结

Go语言中的数组和切片是处理序列数据的重要工具。理解它们作为值类型和引用类型的本质差异,以及这种差异如何影响赋值和函数参数传递,是编写健壮、可预测Go程序的基石。切片因其灵活性和对底层数组的引用特性而更常用于动态数据处理,但这也要求开发者时刻注意其共享底层数据的行为,避免意外的副作用。

相关专题

更多
string转int
string转int

在编程中,我们经常会遇到需要将字符串(str)转换为整数(int)的情况。这可能是因为我们需要对字符串进行数值计算,或者需要将用户输入的字符串转换为整数进行处理。php中文网给大家带来了相关的教程以及文章,欢迎大家前来学习阅读。

312

2023.08.02

int占多少字节
int占多少字节

int占4个字节,意味着一个int变量可以存储范围在-2,147,483,648到2,147,483,647之间的整数值,在某些情况下也可能是2个字节或8个字节,int是一种常用的数据类型,用于表示整数,需要根据具体情况选择合适的数据类型,以确保程序的正确性和性能。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

522

2024.08.29

c++怎么把double转成int
c++怎么把double转成int

本专题整合了 c++ double相关教程,阅读专题下面的文章了解更多详细内容。

49

2025.08.29

C++中int的含义
C++中int的含义

本专题整合了C++中int相关内容,阅读专题下面的文章了解更多详细内容。

190

2025.08.29

treenode的用法
treenode的用法

​在计算机编程领域,TreeNode是一种常见的数据结构,通常用于构建树形结构。在不同的编程语言中,TreeNode可能有不同的实现方式和用法,通常用于表示树的节点信息。更多关于treenode相关问题详情请看本专题下面的文章。php中文网欢迎大家前来学习。

529

2023.12.01

C++ 高效算法与数据结构
C++ 高效算法与数据结构

本专题讲解 C++ 中常用算法与数据结构的实现与优化,涵盖排序算法(快速排序、归并排序)、查找算法、图算法、动态规划、贪心算法等,并结合实际案例分析如何选择最优算法来提高程序效率。通过深入理解数据结构(链表、树、堆、哈希表等),帮助开发者提升 在复杂应用中的算法设计与性能优化能力。

12

2025.12.22

length函数用法
length函数用法

length函数用于返回指定字符串的字符数或字节数。可以用于计算字符串的长度,以便在查询和处理字符串数据时进行操作和判断。 需要注意的是length函数计算的是字符串的字符数,而不是字节数。对于多字节字符集,一个字符可能由多个字节组成。因此,length函数在计算字符串长度时会将多字节字符作为一个字符来计算。更多关于length函数的用法,大家可以阅读本专题下面的文章。

906

2023.09.19

Go中Type关键字的用法
Go中Type关键字的用法

Go中Type关键字的用法有定义新的类型别名或者创建新的结构体类型。本专题为大家提供Go相关的文章、下载、课程内容,供大家免费下载体验。

233

2023.09.06

php源码安装教程大全
php源码安装教程大全

本专题整合了php源码安装教程,阅读专题下面的文章了解更多详细内容。

149

2025.12.31

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Rust 教程
Rust 教程

共28课时 | 4万人学习

Kotlin 教程
Kotlin 教程

共23课时 | 2.2万人学习

Go 教程
Go 教程

共32课时 | 3.2万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号