go语言学习(数组与切片)

数组

一维数组

先来看标准的数组定义方式:

go 复制代码
package main

import "fmt"

func main() {
    var nameList [3]string = [3]string{"sxk", "kunkun", "Y_Yao"}
    fmt.Println(nameList) // [sxk kunkun Y_Yao]
}

数组也可使用简短声明:

go 复制代码
arr := [3]int{1, 2, 3}
fmt.Println(arr) // [1,2,3]

也可以不写长度,让编译器自己判断:(len函数会返回数组的长度)

go 复制代码
arr1 := [...]int{1, 2, 3}
fmt.Println(arr1)      // [1,2,3]
fmt.Println(len(arr1)) //3

注意:长度省略的时候一定要初始化,编译器才能自己推断,不初始化会编译报错!!!。

注意:数组不写长度时,方括号里一定要写... 这是为了和切片区分,切片后面会讲。

还可以指定下标初始化,其他未指定的地方,为零值:

go 复制代码
	arr3 := [...]int{1: 8, 5: 8, 9: 8}
	fmt.Println(arr3)      //[0 8 0 0 0 8 0 0 0 8]
	fmt.Println(len(arr3)) //10

此时数组长度就是最大下标+1。

二维数组
go 复制代码
// 二维数组
var matrix [3][3]int
matrix = [3][3]int{
    {1, 2, 3},
    {4, 5, 6},
    {7, 8, 9},
}
fmt.Println(matrix) //[[1 2 3] [4 5 6] [7 8 9]]

// 也可以简写
matrix2 := [2][2]int{{1, 2}, {3, 4}}
fmt.Println(matrix2) //[[1 2] [3 4]]

也可以省略行,让编译器自己推断:

go 复制代码
matrix3 := [...][2]int{{1, 2}, {3, 4}, {5, 6}}
fmt.Println(matrix3)                   //[[1 2] [3 4] [5 6]]
fmt.Printf("行数:%d\n", len(matrix3))    //行数:3
fmt.Printf("列数:%d\n", len(matrix3[0])) //列数:2

注意:二维数组的定义,可以省略行(用...),但一定不能省略列!!!

为什么可以省略行,而不能省略列?下面来深入理解一下:

其实很多语言都有这样的特性,比如c语言的二维数组,也是可以省略行,不能省略列。

主要看这门语言是编译时确定类型 (静态语言),还是运行时确定类型(动态语言)。go和c语言都是编译时确定,像Python/JavaScript等动态语言都是运行时确定。

先来看看,编译时确定类型的语言省略行时是如何推到行的:

go 复制代码
// 当前合法的语法:
arr1 := [...][3]int{
    {1, 2, 3},    // ← 这是一个元素
    {4, 5, 6},    // ← 这是另一个元素
}

// 每个 {1, 2, 3} 是一个完整的 [3]int
// 编译器看到的是:有2个 [3]int 元素
// 所以能推断出行数 = 2

如上例子,编译器在编译期就知道,arr1是个数组,里面的每个元素都是[3]int类型,它只需数一下arr1里面有几个[3]int就知道行数了。

现在设想一下,如果不省略行,省略列。

go 复制代码
//设想,实际编译会报错
arr2 := [2][...]int{
    {1, 2, 3},
    {4, 5, 6},
}

按理来说,知道行推导列,其实也不难。

首先编译器知道arr2是个数组,有两行,里面每个元素都是[...]int,[...]int是个不确定的类型,所以还需要往里推导一下,发现里面有3个元素,进而推导出,arr2是个数组,有2行,里面的每个元素都是[3]int类型。

从技术的角度上看起来不难实现。

但是实际编译期确定类型的语言(静态语言)有严格的类型系统的约束:

变量在声明时必须确定类型

再回到刚才设想的案例:

首先编译器知道arr2是个数组,有两行,里面每个元素都是[...]int,[...]int是个不确定的类型,此时就已经违反类型系统的约束了,直接报错。

静态语言为什么要有这样的约束?

因为编译器是"死脑筋",它没有人类的推理能力,只能在编译时确定一切。

go 复制代码
// 你写:
var x = 10

// 编译器想:
1. var x = 10
2. 10 是什么?是整数
3. 整数占用多少内存?4字节(32位)或8字节(64位)
4. 记录:x → 地址0x1000~0x1008,类型int
5. 这个地址以后只能存整数
go 复制代码
// 编译时
var x = 10    // 编译时确定x是int
x = "hello"   // 编译错误:不能把字符串赋值给int
// 编译器在编译时就锁定了x的类型

如果是动态语言:

python 复制代码
运行时
x = 10        # 现在x是int
x = "hello"   # 现在x变成str
x = 3.14      # 现在x变成float
# Python解释器在运行时处理类型变化

切片

✅ 切片是数组的"视图"

✅ 切片本身只包含指针、长度、容量

✅ 没有独立存在的切片,有切片就一定有底层数组

✅ 所有切片操作最终都作用在某个数组上

先来看看标准切片的定义:

go 复制代码
package main

import "fmt"

func main() {
    var slice1 []int = []int{1, 2, 3}
    fmt.Println(slice1) //[1 2 3]
    
    //简写
    slice2 := []int{1, 2, 3}
    fmt.Println(slice2) //[1 2 3]
}

可以注意到,切片和数组定义的区别就是切片[]里什么也没有,这也是为什么数组省略长度时必须加...用来区分切片的原因。

切片其实可以理解成一个数组的窗口,其实就是数组的一部分,因为它还可以这样定义:

go 复制代码
var arr [5]int = [5]int{1, 2, 3, 4, 5}
//整个数组的切片
var slice1 []int = arr[:]
//数组前两个元素的切片
var slice2 []int = arr[:2]
//数组后两个元素的切片
var slice3 []int = arr[2:]
//数组中间3个元素的切片
var slice4 []int = arr[1:4] //左闭右开

fmt.Println(slice1) //[1 2 3 4 5]
fmt.Println(slice2) //[1 2]
fmt.Println(slice3) //[3 4 5]
fmt.Println(slice4) //[2 3 4]

还可以通过make函数创建:

go 复制代码
//创建切片,长度为5,容量是10,里面元素默认为零值
slice5 := make([]int, 5, 10)
fmt.Println(slice5) //[0 0 0 0 0]

切片实现的底层可以大致理解成如下结构体

go 复制代码
type sliceHeader struct {
    Data unsafe.Pointer  // 指向底层数组元素的指针
    Len  int             // 切片长度
    Cap  int             // 切片容量
}

与数组不同的是,切片可以扩容,数组的长度是固定的。切片的扩容依赖append函数,且可能会影响原来数组的类型,场景如下:

go 复制代码
package main

import "fmt"

func main() {
    var arr [5]int = [5]int{1, 2, 3, 4, 5}

    fmt.Println("初始状态:")
    fmt.Printf("arr地址: %p, 值: %v\n", &arr, arr)

    // 切片1
    slices1 := arr[:2]
    fmt.Printf("\n创建切片 slices1 = arr[:2]\n")
    fmt.Printf("slices1地址: %p, 值: %v, len=%d, cap=%d\n",
       &slices1[0], slices1, len(slices1), cap(slices1))

    // 第一次 append
    fmt.Printf("\n执行 slices1 = append(slices1, 1)\n")
    slices1 = append(slices1, 1)
    fmt.Printf("slices1地址: %p, 值: %v, len=%d, cap=%d\n",
       &slices1[0], slices1, len(slices1), cap(slices1))
    fmt.Printf("arr值: %v (被修改!)\n", arr)

    // 第二次 append
    fmt.Printf("\n执行 slices1 = append(slices1, 2, 3, 4)\n")
    slices1 = append(slices1, 2, 3, 4)
    fmt.Printf("slices1地址: %p, 值: %v, len=%d, cap=%d\n",
       &slices1[0], slices1, len(slices1), cap(slices1))
    fmt.Printf("arr值: %v (不再被引用)\n", arr)
}

注意:append函数的返回值必须接收,覆盖旧切片,因为切片是值传递,传递的是切片的副本,返回新切片,需要将新切片赋值给旧切片。

输出如下:

对切片尾插时,如果容量够,会直接在切片后面追加,并且切片指向数组,切片后的那个元素会被覆盖修改。

如果容量不足,编译器会在堆上创建一个新数组,新数组的大小一般是原来的两倍(当数组大小>1024时是1.25倍),再将旧数组数据拷贝到新数组,然后在新数组上创建新的切片返回出来。这时旧数组上就没有切片了。

既然切片可以尾插,那是不是可以尾删?其实确实可以,但是没有函数调用:

尾删的本质是减小切片的长度 ,而不改变底层数组(删除的元素在底层数组中仍然存在,只是不在切片范围内了)。

可以通过重新切片实现:

复制代码
package main

import "fmt"

func main() {
    s := []int{1, 2, 3, 4, 5}
    fmt.Println("原切片:", s)  // [1 2 3 4 5]

    // 尾删:删除最后一个元素
    s = s[:len(s)-1]  // 长度减1
    fmt.Println("尾删后:", s)  // [1 2 3 4]
}

不改变底层数组*(删除的元素在底层数组中仍然存在,只是不在切片范围内了)。

可以通过重新切片实现:

复制代码
package main

import "fmt"

func main() {
    s := []int{1, 2, 3, 4, 5}
    fmt.Println("原切片:", s)  // [1 2 3 4 5]

    // 尾删:删除最后一个元素
    s = s[:len(s)-1]  // 长度减1
    fmt.Println("尾删后:", s)  // [1 2 3 4]
}
相关推荐
qeen879 分钟前
【数据结构】建堆的时间复杂度讨论与TOP-K问题
c语言·数据结构·c++·学习·
莎士比亚的文学花园12 分钟前
Linux驱动开发(3)——设备树
开发语言·javascript·ecmascript
图码20 分钟前
如何用多种方法判断字符串是否为回文?
开发语言·数据结构·c++·算法·阿里云·线性回归·数字雕刻
U盘失踪了26 分钟前
python curl转python脚本
开发语言·chrome·python
charlie11451419126 分钟前
Linux 字符设备驱动:cdev、设备号与设备模型
linux·开发语言·驱动开发·c
handler0128 分钟前
Linux 内核剖析:进程优先级、上下文切换与 O(1) 调度算法
linux·运维·c语言·开发语言·c++·笔记·算法
FQNmxDG4S29 分钟前
Java泛型编程:类型擦除与泛型方法的应用场景
java·开发语言·python
lizhihai_991 小时前
股市学习心得-六张分时保命图
大数据·人工智能·学习
我星期八休息1 小时前
IT疑难杂症诊疗室:AI时代工程师Superpowers进化论
linux·开发语言·数据结构·人工智能·python·散列表
热心网友俣先生1 小时前
2026年第二十三届五一数学建模竞赛C题超详细解题思路+各问题可用模型推荐+部分模型结果展示
c语言·开发语言·数学建模