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]
}
相关推荐
宠..15 小时前
QComboBox 方法大全
开发语言·qt
他们叫我阿冠15 小时前
Day4学习--MySQL高级
数据库·学习·mysql
晚烛15 小时前
CANN 模型热更新:不停机模型切换与无缝更新实战指南
开发语言·python
谢白羽15 小时前
agent memory论文解析一:解析项目(a-mem)
开发语言·php·论文·agent·a-mem·实际项目
GEO从入门到精通15 小时前
GEO学习书籍或文章推荐哪本?
人工智能·学习
xian_wwq15 小时前
【学习笔记】“网络空间安全≠网络安全”——ISO SC27标准组十四年仍在求解的不等式
笔记·学习·安全
迷渡15 小时前
用 Rust 重写的 Bun 有 13365 个 unsafe!
开发语言·后端·rust
心中有国也有家16 小时前
pytorch-adapter:让 PyTorch 模型“无缝”跑在昇腾 NPU 上
人工智能·pytorch·笔记·python·学习
吃好睡好便好16 小时前
在Matlab中绘制质点三维运动轨迹图
开发语言·学习·matlab·信息可视化
代码村新手16 小时前
C++-多态
开发语言·c++