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]
}
相关推荐
小陈工2 小时前
Python Web开发入门(十八):跨域问题解决方案——从“为什么我的请求被拦了“到“我让浏览器乖乖听话“
开发语言·python·机器学习·架构·数据挖掘·回归·状态模式
m0_497214152 小时前
Qt事件系统
开发语言·qt
AI科技星2 小时前
全维度相对论推导、光速螺旋时空与北斗 GEO 钟差的统一理论
开发语言·线性代数·算法·机器学习·数学建模
Chef_Chen2 小时前
Agent学习--LLM--推理熵
人工智能·学习·机器学习
ECT-OS-JiuHuaShan2 小时前
科学的本来意义,是基于规范的共识逻辑,而非共识方法
人工智能·科技·学习·算法·生活
赵优秀一一2 小时前
Python 工程化基础1:环境(conda)、pip、requirements.txt
linux·开发语言·python
知识分享小能手2 小时前
MongoDB入门学习教程,从入门到精通,MongoDB的了解应用程序的动态(18)
数据库·学习·mongodb
li1670902702 小时前
第十章:list
c语言·开发语言·数据结构·c++·算法·list·visual studio
游乐码2 小时前
C#List
开发语言·c#·list