Go-基础回顾-复合数据类型-Slice

124 阅读7分钟

复合类型

数组 Array

概念(某百科)

数组(Array)是有序的元素序列。若将有限个类型相同的变量的集合命名,那么这个名称为数组名。组成数组的各个变量称为数组的分量,也称为数组的元素,有时也称为下标变量。用于区分数组的各个元素的数字编号称为下标。数组是在程序设计中,为了处理方便,把具有相同类型的若干元素按有序的形式组织起来的一种形式。这些有序排列的同类数据元素的集合称为数组。数组是用于储存多个相同类型数据的集合。

数组分为==定长数组==和==可变长数组==,这里我们重点说明的是定长数组,因为Go中可变长数组是另一种类型-slice(切片)。

定长数组 注意点

  • 数组:是同一种数据类型的固定长度的序列。
  • 数组定义:var a [len]int,比如:var a [5]int,数组长度必须是常量,且是类型的组成部分。一旦定义,长度不能变。
  • 长度是数组类型的一部分,因此,var a[5] int和var a[10]int是不同的类型。
  • 数组可以通过下标进行访问,下标是从0开始,最后一个元素下标是:len-1
  • 访问越界,如果下标在数组合法范围之外,则触发访问越界,会panic
  • 数组是值类型,赋值和传参会复制整个数组,而不是指针。因此改变副本的值,不会改变本身的值。
  • 支持 "=="、"!=" 操作符,因为内存总是被初始化过的。
  • 指针数组 [n]*T,数组指针 *[n]T。
  • 由于为定长数组所以len和cap返回值相同。
  • 不同长度的数组为不同的数组,不能互相拷贝
    • 因为长度不同,会导致短数组下标越界

数组遍历形式

  • for i := 0; i < len(a); i++ {}
  • for index, v := range a {}

推荐使用第二种,可以避免数组越界;

数组的灵活用法

  • 另类循环
var times [5][0]int
for range times {
    fmt.Println("hello")
}

var arr = [4]int{1,2,3,4}
fmt.Println(unsafe.Sizeof(arr)) //Stdout 32

fmt.Println(unsafe.Sizeof(times)) //Stdout 0 所占内存为0
  • 不占空间的数组还可以用来做通道的值
    • 因为不占空间,因此通道传递过程中减少了赋值操作

说明 times对应的一个数组[5][0]int类型的数组,虽然第一维有长度,但是数组的元素[0]int大小是0,因此整个数组占用的内存大小依然为0。不用付出额外的内存代价,我理解这样写法上也较为简洁。

//数组声明及使用
var arr = [4]int{1,2,3,4}
fmt.Println(arr)

Stdout
[1 2 3 4]

fmt.Println(arr[5]); //直接这样使用 - 编译器可提示错误

//测试下标越界
for i := 0; i <= 5; i++ {
	fmt.Println(arr[i])
}

Stdout
1
2
3
4
panic: runtime error: index out of range [4] with length 4

//值拷贝测试
func testCopyArr(arr []int) {
	fmt.Printf("%p \n", &arr)
}

func main() {
	//数组内存值拷贝测试
	var ar = []int{1,2,3,4}
	fmt.Printf("%p \n", &ar)
	testCopyArr(ar)
}

Stdout
0xc0000a6020 
0xc0000a6040

//内存地址发生变化了,因此证明testCopyArr中的数组并不是main中的数组,而是复制之后的新数组。

// 数组比较
var a = [3]int{1,2,3}
var b = a
fmt.Printf("%b", a == b)

Stdout
%!b(bool=true)

//数组遍历
var a = []int{1,2,3,4}
for i := range a {
	fmt.Printf("a[%d]: %d \n", i, a[i])
}

for i,v := range  a {
	fmt.Printf("a[%d]: %d \n", i, v)
}

for i := 0; i < len(a); i++ {
	fmt.Printf("a[%d]: %d \n", i, a[i])
}
Stdout
略

以上数组,均以int数组为例。

其他数组形式

//字符数组
var s1 =  [2]string{"hello", "world"}
var s2 = [...]string{"你好", "世界"}
var s3 = [...]string{0:"你好", 1:"世界"}

//结构体数组
var struct1 [2]image.Point
var struct2 = [...]image.Point{{0,0}, {1,1}}

// 函数数组
var funcArr1 [2]func(io.Reader) (image.Image, error)
var funcArr2 = [...]func(io.Reader) (image.Image, error) {
    png.Decode,
    jpeg.Decode,
}

// 接口数组
var unknown1 [2]interface{}
var unknown2 = [...]interface{}{123, "你好"}

// 通道数组
var chanList = [2]chan int{}

切片 - slice

概念

简单理解,slice就是一个动态数组(可变长度数组)。所以slice并不是数组或数组指针。它是通过内部指针和相关属性引用数组片段,以实现变长的方案。

可变长数组 - 注意点

  • 切片:切片是数组的一个引用,因此切片是引用类型。但自身是结构体,值拷贝传递。
  • 切片的长度可以改变,因此,切片是一个可变的数组。
  • 切片遍历方式和数组一样,可以用len()求长度。表示可用元素数量,读写操作不能超过该限制。
  • cap可以求出slice最大扩张容量,不能超出数组限制。0 <= len(slice) <= len(array),其中array是slice引用的数组。
  • 切片的定义:var 变量名 []类型,比如 var str []string var arr []int。
  • 如果 slice == nil,那么 len、cap 结果都等于 0。

遍历切片的方式

同数组

切片截取说明

image.png

注: low,high 遵循前包后不包原则

切片操作

  • 添加 append
    • 概念
      • append本质是为了==追加元素==,并不是为了扩容(只是追加的==副作用==)
      • 容量不足时,append操作会导致重新分配内存,可能导致巨大的内存分配和复制数据的代价。
    • 插入方式
      • 头部插入
      • 尾部插入
        • 在开头一般都会导致内存的重新分配,而且会导致已有的元素全部复制一次。
      • 链式操作
        • 多次append会造成临时生成切片
        • 利用copy和append结合避免临时生成切片
  • 删除
    • 本质是移动切片的low或者high
//切片使用
var a []int  //nil 切片;和 nil 相等
fmt.Printf("%b \n", a == nil) //%!b(bool=true)

var b = []int{} // 空切片 和 nil 不相等
fmt.Printf("%b \n", b == nil) // %!b(bool=false)

var c = []int{1,2,3} // 3个元素的切片 len cap 都是3
fmt.Printf("len: %d; cap: %d \n", len(c), cap(c)) //len: 3; cap: 3

var d = c[:2] // 有2个元素的切片 len 2 cap 3
fmt.Printf("len: %d; cap: %d; %v \n", len(d), cap(d), d) //len: 2; cap: 3 [1 2]

var e = c[0:2] // 有两个元素的切片 len 2 cap 3
fmt.Printf("len: %d; cap: %d; %v \n", len(e), cap(e), e) //len: 2; cap: 3 [1 2]

var f = c[:0] // 有两个元素的切片 len 0 cap 3
fmt.Printf("len: %d; cap: %d; %v \n", len(f), cap(f), f) //len: 0; cap: 3 []

var g = make([]int, 3) // 有三个元素的切片 len = 3 cap = 3 初始化为 0 0 0
fmt.Printf("len: %d; cap: %d; %v \n", len(g), cap(g), g) //len: 3; cap: 3 [0 0 0]

var h = make([]int, 2, 3) // 有三个元素的切片 len = 2 cap = 3 初始化为 0 0
fmt.Printf("len: %d; cap: %d; %v \n", len(h), cap(h), h) //len: 2; cap: 3 [0 0]

var i = make([]int, 0, 3) // 有三个元素的切片 len = 0 cap = 3
fmt.Printf("len: %d; cap: %d; %v \n", len(i), cap(i), i) //len: 0; cap: 3 []


var arr = []int{1,2,3,4}
fmt.Println(arr)

// 切片添加
var a []int
// 尾部插入
a = append(a, 1)
fmt.Printf("%v \n", a) // [1] 
a = append(a, 1, 2, 3)
fmt.Printf("%v \n", a) // [1 1 2 3]
a = append(a, []int{1,2,3}...)
fmt.Printf("%v \n", a) // [1 1 2 3 1 2 3] 

// 头部插入
a = append([]int{0}, a...)
fmt.Printf("%v \n", a) // [0 1 1 2 3 1 2 3] 
a = append([]int{-3,-2,-1}, a...)
fmt.Printf("%v \n", a) // [-3 -2 -1 0 1 1 2 3 1 2 3]

// 链式操作
a = append(a[:0], append([]int{0}, a...)...)
fmt.Printf("%v", a) // [0 -3 -2 -1 0 1 1 2 3 1 2 3]

// 避免生成临时切片,但是代码写的实在是有点冗长……,实际应用中应该用不到
i := 0
var b = []int{1,2,3}
b = append(b, 0)
copy(b[i+1:], b[i:])
b[i] = 1
fmt.Printf("%v \n", b) //[1 1 2 3]


//删除元素
var a = []int{1,2,3,4}
a = a[:len(a) - 1]//删除尾部第一个元素
fmt.Printf("%v \n", a) //[1 2 3]

a = a[1:] //删除头部第一个元素
fmt.Printf("%v \n", a) //[2 3]

  • Go语言实现中非0大小数组的长度不得超过2GB,因此需要针对数组元素的类型大小计算数组的最大长度范围([]uint8最大2GB,[]uint16最大1GB,依此类推,
  • []struct{}数组的长度可以超过2GB

字典 - map - 待续。。。

结构体 - struct

指针 - pointer

参考