数组和切片的相关操作

Golang基础里面关于复合数据类型 - 数组和切片的细节知识点,同时面试可能被问到,所以在此整理总结下来。

首先,在谈slice(切片)之前,必须先提一下与之密不可分的另一数据结构 — array(数组)。

注:复合数据类型即由零个或多个元素组成的值,其中每个元素都有一个属于特定数据类型的值。

array(数组)

数组是由同构的元素组成——每个数组元素都是完全相同的类型——结构体则是由异构的元素组成的。数组和结构体都是有固定内存大小的数据结构。相比之下,slice和map则是动态的数据结构,它们将根据需要动态增长。

数组是一个由固定长度的特定类型元素组成的序列,一个数组可以由零个或多个元素组成。由于数组的长度是固定的,因而在使用的时候我们用的最多的是slice(切片),它是可以增长和收缩动态序列,slice功能也更灵活,但是要理解slice工作原理的话需要先理解数组。

数组的每个元素可以通过索引下标来访问,索引下标的范围是从0开始到数组长度减1的位置。内置的len函数将返回数组中元素的个数。

数组的每个元素都被初始化为元素类型对应的零值,对于数字类型来说就是0。

var m [3]int = [3]int{1, 2, 3}
var n [3]int = [3]int{1, 2}
fmt.Println(m[2]) // 3
fmt.Println(n[2]) // 0

在数组字面值中,如果在数组的长度位置出现的是“…”省略号,则表示数组的长度是根据初始化值的 个数来计算。

m := [...]int{1, 2, 3}
fmt.Printf("%T\n", m) // [3]int

数组的长度是数组类型的一个组成部分,因此[3]int[4]int是两种不同的数组类型。数组的长度必须是常量表达式,因为数组的长度需要在编译阶段确定。

数组可以直接进行比较,当数组内的元素都一样的时候表示两个数组相等。

arr1 := [3]int{1, 2, 3}
arr2 := [3]int{1, 2, 3}
arr3 := [3]int{1, 2, 4}
fmt.Println(arr1 == arr2, arr1 == arr3)  // true,false

数组可以作为函数的参数传入。但由于数组在作为参数的时候,其实是进行了拷贝,这样在函数内部改变数组的值,是不影响到外面的数组的值的。

func ArrIsArgs(arr [4]int) {
	arr[0] = 120
}

func main() {
	m := [...]int{1, 2, 3, 4}
	ArrIsArgs(m)
	fmt.Println(m)  // [1 2 3 4]
}

如果想要改变就只能使用指针,在函数内部改变数组的值,也会改变外面的数组的值:

func ArrIsArgs(arr *[4]int) {
	arr[0] = 120
}

func main() {
	m := [...]int{1, 2, 3, 4}
	ArrIsArgs(&m)
	fmt.Println(m)  // [120 2 3 4]
}

通常这样的情况下都是用切片来解决,而不是用数组。

slice(切片)

Slice(切片)代表变长的序列,序列中每个元素都有相同的类型。一个slice类型一般写作[]T,其中 T 代表 slice 中元素的类型;slice的语法和数组很像,只是没有固定长度而已。

数组和 slice 关系非常密切,一个 slice 可以访问数组的部分或者全部数据,而且slice的底层本身就是对数组的引用

一个Slice由三部分组成:指针,长度和容量。内置的lencap函数可以分别返回slice的长度和容量。指针指向第一个slice元素对应的底层数组元素的地址,要注意的是slice的第一个元素并不一定就是数组的第一个元素。长度对应slice中元素的数目。长度不能超过容量,容量一般是从slice的开始位置到底层数据的结尾位置。

slice的切片操作s[i:j],其中0 ≤ i≤ j≤ cap(s),用于创建一个新的 slice,引用 s 的从第 i 个元素开始到第 j-1 个元素的子序列。新的slice将只有 j-i 个元素。如果 i 位置的索引被省略的话将使用0代替,如果 j 位置的索引被省略的话将使用 len(s) 代替。 如果切片操作超出 cap(s) 的上限将导致一个panic异常,但是超出 len(s) 则是意味着扩展了 slice,因为新 slice 的长度会变大。

注:使用slice的时候容易犯的错误, 如下示例:

func main() {
	p:=[]int{1,2,3}
	fmt.Println("p:",p)  // [1 2 3]

	m1 :=p[:2]
	fmt.Println("m1:",m1)  // [1, 2]

	m2 := m1[1:]
	fmt.Println("m2:",m2)  // [2]

	m3 := m2[:2]
	m4 := p[:2][1:][:2]
	fmt.Println("m3",m3)  // [2, 3]
	fmt.Println("m4",m4)  // [2, 3]
}

疑问:m4中p[:2][1:]已经等价于m2中的截取到的值[2]了,为何还可以再继续截取m2[:2]==m4 操作,其实这里就要说到切片的原因,slice的底层本身就是对数组的引用,因而多个 slice 共享的是底层的同一个数组。虽然 m4 表面上没有,但是如果指定了截止的位置并且这个位置没有超过底层数组的范围,它就会指针引用底层的数组,这还是可以取到底层数组的值。

slice创建方式主要有两种:1.基于数组创建。2.直接创建。

  • 基于数组创建
arrVar := [4]int{1, 2, 3, 4}
sliceVar := arrVar[1:3]

数组 arrVar 和 sliceVar 里面的地址其实是一样的,因而如果改变 sliceVar 里面的变量,那么 arrVar 里面的变量也会随之改变。

arrVar := [4]int{1, 2, 3, 4}
sliceVar := arrVar[1:3]
sliceVar[1] = 10
fmt.Printf("arrVar: %d, sliceVar: %d", arrVar, sliceVar)  // arrVar: [1 2 10 4], sliceVar: [2 10]
  • 直接创建
  1. 内建函数new分配了零值填充的元素类型的内存空间,并且返回其地址,一个指针类型的值。
var p *[]int = new([]int)  // 分配slice结构内存.
var  []int = make([]int,100) // m指向一个新分配的有100个整数的数组.

因此:

new 分配内存空间;
make 初始化;
new(T) 返回 *T 指向一个零值 T;
make(T) 返回初始化后的 T.

注:make仅适用于 map,slice 和 channel,并且返回的不是指针。获得特定的指针应当用 new 。

  1. 内置的make函数创建一个指定元素类型、长度和容量的slice。容量部分可以省略,在这种情况下容量将等于长度。

使用内置的make()函数来创建。事实上还是会创建一个匿名的数组,只是不需要我们来定义。

make([]T, len)
make([]T, len, cap) // same as make([]T, cap)[:len]

在底层, make 创建了一个匿名的数组变量,然后返回一个 slice; 只有通过返回的 slice 才能引用底层匿名的数组变量。在第一种语句中, slice 是整个数组的 view。在第二个语句中, slice 只引用了底层数组的前 len 个元素,但是容量将包含整个的数组。额外的元素是留给未来的增长用的。

slice1 := make([ ]int,5)//创建一个元素个数5的slice,cap也是5
slice2 := make([ ]int,5,10)//创建一个元素个数5的slice,cap是10
slice3 := []int{1,2,3,4,5}//创建一个元素个数为5的slice,cap是5
var slice []int //创建一个空的slice,cap和len都是0

slice切片的操作

一般操作

  1. 声明变量,go自动初始化为nil,长度:0,地址:0,nil
func main() {
	var ss []string
	fmt.Printf("length: %v \taddr: %p \tisnil: %v", len(ss), ss, ss == nil)  // length: 0 	addr: 0x0 	isnil: true
}
  1. 切片的追加,删除,插入操作
func main() {
	var ss []string
	fmt.Printf("[local print]:\t length: %v \taddr: %p \tisnil: %v", len(ss), ss, ss == nil)
	fmt.Println()

	// 切片尾部追加元素, append element
	for i := 0; i < 10; i++ {
		ss = append(ss, fmt.Sprintf("s%d", i))
	}
	fmt.Printf("[append print]:\t length: %v \taddr: %p \tisnil: %v", len(ss), ss, ss == nil)
	fmt.Println()
	fmt.Println(ss)  // [s0 s1 s2 s3 s4 s5 s6 s7 s8 s9]

	// 删除切片元素, remove element at index
	index := 5
	ss = append(ss[:index], ss[index+1:]...)
	fmt.Printf("[delete print]:\t length: %v \taddr: %p \tisnil: %v", len(ss), ss, ss == nil)
	fmt.Println()
	fmt.Println(ss)  // [s0 s1 s2 s3 s4 s6 s7 s8 s9]

	//在切片中间插入元素insert element at index;
	//注意:保存后部剩余元素,必须新建一个临时切片
	rear := append([]string{}, ss[index:]...)
	fmt.Println(rear)  // [s6 s7 s8 s9]
	ss = append(ss[0:index], "inserted")  // [s0 s1 s2 s3 s4 inserted]
	ss = append(ss, rear...) 
	fmt.Println(ss)  // [s0 s1 s2 s3 s4 inserted s6 s7 s8 s9]
}
  1. copy的使用

在使用copy复制切片之前,要保证目标切片有足够的大小,注意是大小,而不是容量

func main() {
	var sa = make([]string, 0)
	for i := 0; i < 10; i++ {
		sa = append(sa, fmt.Sprintf("%v", i))
	}

	var da = make([]string, 0, 10)  // length=0
	var cc = 0
	cc = copy(da, sa)
	fmt.Printf("copy to da(len=%d)\t%v\t\n", len(da), da)

	da = make([]string, 5)
	cc = copy(da, sa)
	fmt.Printf("copy to da(len=%d)\tcopied=%d\t%v\n", len(da), cc, da)

	da = make([]string, 10)
	cc = copy(da, sa)
	fmt.Printf("copy to da(len=%d)\tcopied=%d\t%v\n", len(da), cc, da)
}

---
Runing...

copy to da(len=0)	[]	
copy to da(len=5)	copied=5	[0 1 2 3 4]
copy to da(len=10)	copied=10	[0 1 2 3 4 5 6 7 8 9]

从上面运行结果可以看出,目标切片大小为0,容量为10,copy不能复制。目标切片大小如果小于源切片大小,copy就会按照目标切片大小复制,不会报错。

初始化大小和容量

当我们使用make初始化切片的时候,必须给出size。go语言相关的书上一般都会告诉我们,当切片有足够大小的时候,append操作是非常快的。但是当给出初始大小后,我们得到的实际上是一个含有这个size数量切片类型的空元素。

func main() {
	var ss = make([]string, 10)
	ss = append(ss, "last")
	fmt.Printf("after append: len=%v, addr=%p, isnil=%v, content=%v", len(ss), ss, ss == nil, ss)
}

--- 
Runing... 

after append: len=11, addr=0xc042054140, isnil=false, content=[          last]

实际上,此时我们应该先用下标为切片元素负值。但是如果我们既想有好的效率,又想继续使用append函数而不想区分是否有空的元素,此时就要用到make的第三个参数,容量,也就是我们通过传递给make,0个大小和足够大的容量数值就行了。

func main() {
	var ss = make([]string, 0, 10)
	ss = append(ss, "last")
	fmt.Printf("after append: len=%v, addr=%p, isnil=%v, content=%v", len(ss), ss, ss == nil, ss)
}

---
Runing... 

after append: len=1, addr=0xc0420420a0, isnil=false, content=[last]

切片的指针

  1. 当我们用append追加元素到切片时,如果容量不够,go就会创建一个新的切片变量。
func main() {
	var sa []string
	fmt.Printf("addr: %p \t\t\t\tlen: %v content:%v\n", sa, len(sa), sa)

	for i := 0; i < 10; i++ {
		sa = append(sa, fmt.Sprintf("%v", i))
		fmt.Printf("addr: %p \t\tlen: %v content:%v\n",sa,len(sa),sa);
	}

	fmt.Printf("addr: %p \t\tlen: %v content:%v\n", sa, len(sa), sa)
}

--- 
Runing... 

addr: 0x0 			   len: 0 content:[]
addr: 0xc0420461c0 		len: 1 content:[0]
addr: 0xc04204c440 		len: 2 content:[0 1]
addr: 0xc04204e0c0 		len: 3 content:[0 1 2]
addr: 0xc04204e0c0 		len: 4 content:[0 1 2 3]
addr: 0xc042084000 		len: 5 content:[0 1 2 3 4]
addr: 0xc042084000 		len: 6 content:[0 1 2 3 4 5]
addr: 0xc042084000 		len: 7 content:[0 1 2 3 4 5 6]
addr: 0xc042084000 		len: 8 content:[0 1 2 3 4 5 6 7]
addr: 0xc042086000 		len: 9 content:[0 1 2 3 4 5 6 7 8]
addr: 0xc042086000 		len: 10 content:[0 1 2 3 4 5 6 7 8 9]
addr: 0xc042086000 		len: 10 content:[0 1 2 3 4 5 6 7 8 9]
// 可以看出,很明显切片的地址经过了数次改变。
  1. 如果,在make初始化切片的时候给出了足够的容量,append操作不会创建新的切片。
func main() {
	var sa = make([]string, 0, 10)
	fmt.Printf("addr:%p \t\tlen:%v content:%v\n",sa,len(sa),sa)

	for i := 0; i< 10; i++ {
		sa = append(sa, fmt.Sprintf("%v", i))
		fmt.Printf("addr:%p \t\tlen:%v content:%v\n",sa,len(sa),sa)
	}

	fmt.Printf("addr:%p \t\tlen:%v content:%v\n",sa,len(sa),sa)
}

---
Runing... 

addr:0xc0420480a0 		len:0 content:[]
addr:0xc0420480a0 		len:1 content:[0]
addr:0xc0420480a0 		len:2 content:[0 1]
addr:0xc0420480a0 		len:3 content:[0 1 2]
addr:0xc0420480a0 		len:4 content:[0 1 2 3]
addr:0xc0420480a0 		len:5 content:[0 1 2 3 4]
addr:0xc0420480a0 		len:6 content:[0 1 2 3 4 5]
addr:0xc0420480a0 		len:7 content:[0 1 2 3 4 5 6]
addr:0xc0420480a0 		len:8 content:[0 1 2 3 4 5 6 7]
addr:0xc0420480a0 		len:9 content:[0 1 2 3 4 5 6 7 8]
addr:0xc0420480a0 		len:10 content:[0 1 2 3 4 5 6 7 8 9]
addr:0xc0420480a0 		len:10 content:[0 1 2 3 4 5 6 7 8 9]
// 可见,切片的地址一直保持不变.
  1. 如果不能准确预估切片的大小,又不想改变变量(如:为了共享数据的改变),这时候就要请出指针来帮忙了。下面程序中,sa 就是osa 这个切片的指针,我们共享切片数据和操作切片的时候都使用这个切片地址就ok了,其本质上是:append 操作亦然会在需要的时候构造新的切片,不过是将地址都保存到了 sa 中,因此我们通过该指针始终可以访问到真正的数据。
func main() {
	var osa = make([]string, 0)
	sa := &osa

	for i := 0; i< 10; i++ {
		*sa = append(*sa, fmt.Sprintf("%v", i))
		fmt.Printf("addr of osa: %p, \taddr: %p \tcontent:%v\n", osa, sa ,sa)
	}

	fmt.Printf("addr of osa: %p, \taddr: %p \tcontent:%v\n", osa, sa, sa)
}

--- 
Runing... 

addr of osa: 0xc0420461c0, 	addr: 0xc04204c3e0 	content:&[0]
addr of osa: 0xc04204c460, 	addr: 0xc04204c3e0 	content:&[0 1]
addr of osa: 0xc04204e0c0, 	addr: 0xc04204c3e0 	content:&[0 1 2]
addr of osa: 0xc04204e0c0, 	addr: 0xc04204c3e0 	content:&[0 1 2 3]
addr of osa: 0xc042086000, 	addr: 0xc04204c3e0 	content:&[0 1 2 3 4]
addr of osa: 0xc042086000, 	addr: 0xc04204c3e0 	content:&[0 1 2 3 4 5]
addr of osa: 0xc042086000, 	addr: 0xc04204c3e0 	content:&[0 1 2 3 4 5 6]
addr of osa: 0xc042086000, 	addr: 0xc04204c3e0 	content:&[0 1 2 3 4 5 6 7]
addr of osa: 0xc042088000, 	addr: 0xc04204c3e0 	content:&[0 1 2 3 4 5 6 7 8]
addr of osa: 0xc042088000, 	addr: 0xc04204c3e0 	content:&[0 1 2 3 4 5 6 7 8 9]
addr of osa: 0xc042088000, 	addr: 0xc04204c3e0 	content:&[0 1 2 3 4 5 6 7 8 9]

(完)

PREVIOUSGolang变量作用域
NEXTSublime编辑器的Golang插件