Go中的数组和切片

本文参考:
https://www.liwenzhou.com/posts/Go/05_array/
https://www.liwenzhou.com/posts/Go/06_slice/

Array(数组)

数组是长度固定,类型固定的数据容器,根据下标访问和修改元素内容,下标从0开始,最后一个元素的下标是数组长度减1。可以使用len(arr)获得数组长度。

基本语法:

// 定义一个长度为3元素类型为int的数组a
var a [3]int   // [0,0,0]

数组定义

var 数组变量名 [元素数量]T

比如:var a [3]int,数组长度必须是常量,并且长度是数组类型的一部分,一旦定义,长度不能改变。[3]int[5]int是两个不同的类型。

var a [3]int
var b [4]int
a = b // 错误语法,此时a和b是不通的类型

数组的初始化

方法一

初始化数组时可以使用初始化列表来设置数组元素的值

func main(){
	var a1 [3]int   //数组会初始化int类型的零值
    fmt.Println(a1)   // [0,0,0]
    var a2 [3]int{1,2}  // 使用指定的初始值完成初始化
    fmt.Println(a2)     // [1,2,0]
    var a3 [3]string{"Aliex","Negan","Egon"}  // 使用指定的初始值完成初始化
    fmt.Println(a3)    // ["Aliex","Negan","Egon"]
}

方法二

按照上面的方法每次都要确保提供的初始值和数组长度保持一致,一般情况下我们会让编译器根据我们的初始值的个数自行推断数组的长度。

func main(){
    // 开辟长度为3的整型数组
    var a1 [3]int = [3]int{}  
    fmt.Println(a1)  // [0,0,0]
    var a2 [...]int{1,2} 
    fmt.Println(a2)   // [1,2]
    fmt.Printf("type of a2:%T
", a2)   // type if a2:[2]int
}

方法三

使用指定索引值得方式来初始化数组

func main(){
    a := [...]int{1:1,3:5}
    fmt.Println(a)  //[0,1,0,5]
    fmt.Printf("type of a:%T
",a)  // type of a:[4]int
}

数组的遍历

数组遍历方式有两种,可以通过长度和下标去遍历,还可以通过range关键字,以枚举的方式去遍

长度和下标

func main(){
	var city = [...]string{"北京","上海",“西安”}
	for i:=0;i<len(a);i++{
		fmt.Println(a[i])
	}
}

枚举

func main(){
	var city = [...]string{"北京","上海",“西安”}
	for index,value := range city{
		fmt.Println(index,value)
	}
	
}

多维数组

Go语言是支持多维数组的,这里我们以二维数组为例(数组中又嵌套数组)

二维数组的定义

func main(){
	city := [3][2]{
		{"陕西","西安"},
		{"宁夏","银川"},
		{"甘肃","兰州"},
	}
	fmt.Println(a)  // [[陕西,西安] [宁夏,银川] [甘肃,兰州]]
	fmt.Println(a[2][1]) // 支持索引取值:兰州
}

二维数组的遍历

func main(){
	city := [3][2]{
		{"陕西","西安"},
		{"宁夏","银川"},
		{"甘肃","兰州"},
	}
	for _,c1 := range city{
		for _,c2 := range c1{
			fmt.Printf("%s	",c2)
		}
		fmt.Println()
	}
}

输出:

陕西    上海
宁夏    银川
甘肃    兰州

注意:多维数组只有第一层可以使用...来让编译器推到数组长度。例如:

// 支持写法
a := [...][2]string{
	{"陕西","西安"},
	{"宁夏","银川"},
	{"甘肃","兰州"},
}

// 不支持多维素组的内层使用
b := [3][...]string{
	{"陕西","西安"},
	{"宁夏","银川"},
	{"甘肃","兰州"},
}

数组是值类型

数组是值类型,赋值和传参会复制整个数组,因此改变副本的值,不会改变本身的值。

func modifyArray1(x [3]int){
	x[0] = 100
}

func modifyArray2(x [3][2]int){
	x[2][0] = 100
}

func main(){
	a := [3]int{10,20,30}
	modifyArray1(a)   // 在modify中修改a的副本
	fmt.Println(a)    // [10,20,30] a保持不变
    b := [3][2]int{
    	{1,1},
    	{1,1},
    	{1,1},
    }
    modifyArray2(b)  // 在modify中修改的是b的副本
    fmt.Println(b)   // [[1 1] [1 1] [1 1]]
}

注意:

1.数组支持“==”,“!=”操作符,因为内存总是被初始化过的。

2.[n]*T表示指针数组

3.*[n]T表示数组指针

练习题

1.求数组[1,3,5,7,8]所有元素的和

func main(){
	a := [...]int{1,3,5,7,8}
	sum := 0
    for _,v := range a{
		sum += v
	}
	fmt.Print(sum)
}

2.找出数组中和为指定值得两个元素的下标,比如从数组[1,3,5,7,8]找出和为8的两个元素的下标分别是(0,3)和(1,2)。

func main(){
    a := [...]int{1,3,5,7,8}
    for i,v := range a{
        for l,j := range a[i+1:]{
            if v + j == 8{
                fmt.Print(i,j)
            }
        }
    }
}

Slice(切片)

数组的局限:数组的长度是固定的,并且长度属于类型的一部分。

func sum(x [3]int)int{
	sum := 0
	for _,v := range x{
		sum += v
	}
	return sum
}

这个求和函数只能接受[3]int类型的数组,其他的都不支持。

a ;= [3]int{1,2,3}

上面的数组a已经有三个元素了,我们不能在继续给数组a中添加新的元素。

切片概述

切片可以理解为长度可以动态变化的数组,它是基于数组类型做的一层封装。非常灵活,支持自动扩容。

切片和数组的相同点:

* 通过下标来访问元素
* 通过下标或range方式遍历元素

切片和数组的不同点:

* 可以动态的向切片中追加新的元素

切片中可以容纳元素的个数称为容量,容量大于等于长度,可以通过len(slice)cap(clice)分别获取切片的长度和容量。

可以通过make(type,len,cap)的方式创建出自动以初始长度和容量的切片,在追加元素的过程中,如果容量不够用时,就存在动态扩容问题,动态扩容采用的是倍增策略,即:新容量=2*就容量。扩容后的切片会得到一片新的连续内存地址,所有元素的地址都会随之发生改变。

切片的定义

声明切片类型的语法:

var name []T
// name表示变量名,T表示切片中的元素类型
func main(){
	var a []string   // 声明一个字符串的切片,注意和数组的区别
	var b []int{}    // 声明一个整型切片并初始化
	var c []bool{false,true} // 声明一个布尔切片并初始化
    var d []bool{false,true}
	fmt.Println(a)   //[]
	fmt.Println(b)   //[]
	fmt.Println(c)   //[false,true]
    fmt.Println(a==nil)  // true
    fmt.Println(b==nil)  // false
    fmt.Println(c==nil)  // false
    // fmt.Println(c==d)  //切片是引用类型,不支持直接比较,只能和nil比较
}

切片表达式

切片表达式从字符串、数组、指向数组或切片的指针构造字符串或切片,有两种变体:一种指定start和end两个索引界限值得简单形式,另一种是除了start和end索引界限值外还制定容量的完整形式。

简单切片表达式

切片的底层就是一个数组,我们可以基于数组通过切片表达式得到切片,切片中的startend表示一个索引范围(左包含,右不包含)

func main(){
	a := [5]int{1,2,3,4,5}   //长度为5,数据类型为int的数组
	s := a[1:3]  // s:= a[start:end]
	fmt.Printf("s:%v len(s):%v cap(s):%v
",s,len(s),cap(s))
    //s:[2 3] len(s):2 cap(s):4
}

为了方便,通常可以省略切片表达式中的任何索引,省略了start则默认是0;省略了end则默认到结尾。

a[2:]  // a[2:len(a)]
a[:3]  // a[0:3]
a[:]   // a[0:len(a)]

注意:对于数组或者字符串,如果0<=start<=end<=len(a),则索引合法,否则索引越界(out of range)

对切片再次执行切片表达式时(切片再切片),end的上限边界是切片的容量cap(a),而不是长度。常量索引必须是非负的,并且可以用int类型的值表示;对于数组或常量字符串,常量索引必须在有效范围内,如果startend两个指标都是常数,必须满足start<=end。如果索引在运行时超出范围,就会发生运行时panic

func main(){
	a := [5]int{1,2,3,4,5}
	s1 := a[1:]  // s:=a[start:end]
	fmt.Printf("s1:%v len(s1):%v cap(s1):%v
",s1,len(s1),cap(s1))
    // s1:[2 3 4 5] len(s1):2 cap(s1):4
    s2 := s1[3:4]  // 索引上限是cap(s1)而不是len(s1)
    fmt.Printf("s2:%v len(s2):%v cap(s2):%v
",s2,len(s2),cap(s2))
    // s2:[5] len(s2):1 cap(s2):1
}

完整切片表达式

对于数组,指向数组的指针,或切片(注意不能是字符串)支持完整切片表达式:

a[start:end:max]

上面的代码会构造与简单切片表达式a[start:end]相同类型,相同长度和元素的切片,另外,它会将得到的结果切片的容量设置为max-start。在完整切片表达式中只有第一个索引值(start)可以省略,默认为0

func main(){
	a := [5]int{1,2,3,4,5}
	t := a[1:3:5]
	fmt.Printf("t:%v len(t):%v cap(t):%v
",t,len(t),cap(t))
	// t:[2,3] len(t):2 cap(t):4
}

完整切片表达式需要满足的条件是0<=start<=end<=max<=cap(a),其他条件和简单切片表达式相同。

使用make()函数构造切片

上面都是基于数组来创建的切片,如果需要动态的床架哪一个切片,我们就需要使用内置函数make()

make([]T,size,cap)
// T:切片的元素类型
// size:切片中元素的数量
// cap:切片的容量

示例:

func main(){
	a := make([]int,2,10)
	fmt.Println(a)  // [0,0]
	fmt.Println(len(a))  // 2
	fmt.Println(cap(a))  // 10
}

a的内部存储空间已经分配了10个,但是实际上只用了2个,容量并不会影响当前元素的个数,所以len(a)返回2,cap(a)则返回该切片的容量。

切片的本质

切片的本质就是对底层数组的封装,包含三个信息:底层数组的指针,切片的长度和切片的容量。

现在有一个数组a:=[8]int{0,1,2,3,4,5,6,7},切片s1:=a[:5],相应的示意图如下:

切片s2:=a[3:6],相应的示意图如下:

切片不能直接比较

切片之间是不能比较的,不能使用==操作符来判断两个切片是否含有全部相等元素。切片唯一合法的比较操作适合nil比较,一个nil值的切片并没有底层数组,一个nil值的切片长度和容量都是0,但是不能说一个长度和容量都是0的切片一定是nil

var s1 []int   // len(s1)=0;cap(s1)=0;s1==nil  //定义但未初始化
s2 := []int{}  // len(s2)=0;cap(s2)=0;s2!=nil  //定义且初始化,有了内存地址
s3 := make([]int,0) //len(s3)=0;cap(s3);s3!=nil 

所以要检查切片是否为空,始终使用len(s)==0来判断,不能使用s==nil来判断。

切片的赋值拷贝

下面代码中演示了拷贝前后两个变量共享底层数组,对一个切片的修改会影响另一个切片的内容。(引用传递)

func main(){
	s1 := make([]int, 3) //[0 0 0]
	s2 := s1   //将s1直接赋值给s2,s1和s2共用一个底层数组
	s2[0] = 100
	fmt.Println(s1) [100 0 0]
	fmt.Println(s2) [100 0 0]
}

切片的遍历

切片的遍历方式和数组是一致的,支持索引遍历和for range遍历。

func main(){
	s := []int{2,3,1}
	for i:=0;i<len(s);i++{
		fmt.Println(i, s[i])
	}
	
	for index,value:=range s{
		fmt.Println(index,value)
	}
	
}

向切片中添加元素

Go语言的内建函数append()可以为切片动态添加元素,可以一次添加一个元素,可以添加多个元素,也可以添加另外一个切片中的元素(后面加...)

func main(){
	var s []int
	s = append(s, 1) //s[1]
	s = append(s,2,3,4) // [1 2 3 4]
	s = append(s, s...) // [1 2 3 4 1 2 3 4]
}

注意:通过var声明的零值切片可以在append()函数直接使用无需初始化。没有必要初始化一个切片再传入append()函数使用

s := []int{}  // 没有必要初始化
s = append(s, 1,2,3)

var s = make([]int)  // 没有必要初始化
s = append(s,1,2,3)

每个切片都会指向一个底层数组,这个数组的容量够用就添加新增元素,当底层数组不能容纳新的元素时,切片就会自动按照一定的策略进行“扩容”,此时该切片的底层数组就会更换。“扩容”操作往往发生在append()函数调用时,所以我们通常都需要用原变量接收append函数的返回值。

func main(){
	//append()添加元素和切片扩容
	var numSlice []int
	for i:=0;i<10;i++{
		numSlice = append(numSlice,i)
		fmt.Printf("%v len:%d  cap:%d ptr:%p
", numSlice, len(numSlice),cap(numSlice),numSlice)
	}
}

结论:

  • append()函数将元素追加到切片的最后并返回该切片
  • 切片numSlice的容量按照1,2,4,8,16这样的顾泽自动进行扩容,每次扩容后都是扩容前的两倍。

切片的扩容策略

可以通过查看$GOROOT/src/runtime/slice.go源码,其中扩容相关代码如下:

newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
	newcap = cap
} else {
	if old.len < 1024 {
		newcap = doublecap
	} else {
		// Check 0 < newcap to detect overflow
		// and prevent an infinite loop.
		for 0 < newcap && newcap < cap {
			newcap += newcap / 4
		}
		// Set newcap to the requested cap when
		// the newcap calculation overflowed.
		if newcap <= 0 {
			newcap = cap
		}
	}
}

从上面的代码可以看出以下内容:

  • 首先判断,如果新申请容量(cap)大于2倍的旧容量(old.cap),最终容量(newcap)就是新申请的容量(cap)。
  • 否则判断,如果旧切片的长度小于1024,则最终容量(newcap)就是旧容量(old.cap)的两倍,即(newcap=doublecap),
  • 否则判断,如果旧切片长度大于等于1024,则最终容量(newcap)从旧容量(old.cap)开始循环增加原来的1/4,即(newcap=old.cap,for {newcap += newcap/4})直到最终容量(newcap)大于等于新申请的容量(cap),即(newcap >= cap)
  • 如果最终容量(cap)计算值溢出,则最终容量(cap)就是新申请容量(cap)。

需要注意的是,切片扩容还会根据切片中元素的类型不同而做不同的处理,比如intstring类型的处理方式就不一样。

使用copy()函数复制切片

问题:

func main() {
	a := []int{1, 2, 3, 4, 5}
	b := a
	fmt.Println(a) //[1 2 3 4 5]
	fmt.Println(b) //[1 2 3 4 5]
	b[0] = 100
	fmt.Println(a) //[100 2 3 4 5]
	fmt.Println(b) //[100 2 3 4 5]
}

由于切片是引用类型,a和b其实指向了同一块内存地址,修改b的同时,a的值也会发生改变。

Go语言内建的copy()函数可以迅速地将一个切片的数据复制到另外一个切片空间中,copy()函数的使用格式如下:

copy(destSlice, srcSlice []T)
// srcSlice:数据来源切片
// destSlice:目标切片

示例:

func main() {
	// copy()复制切片
	a := []int{1, 2, 3, 4, 5}
	c := make([]int, 5, 5)
	copy(c, a)     //使用copy()函数将切片a中的元素复制到切片c
	fmt.Println(a) //[1 2 3 4 5]
	fmt.Println(c) //[1 2 3 4 5]
	c[0] = 1000
	fmt.Println(a) //[1 2 3 4 5]
	fmt.Println(c) //[1000 2 3 4 5]
}

从切片中删除元素

Go语言中并没有删除切片元素的专用方法,我们可以通过使用切片本身的特性来删除元素。

func main(){
	// 从切片中删除元素
	a := []int{30,32,34,36,38}
	// 删除索引为2的元素
	a = append(a[:2],a[3:]...)
}

从切片a中删除索引为index的元素,操作方法是a = append(a[:index], a[index+1:]...)

练习题

  1. 请写出下面代码运行结果

    func main() {
    	var a = make([]string, 5, 10)
    	for i := 0; i < 10; i++ {
    		a = append(a, fmt.Sprintf("%v", i))
    	}
    	fmt.Println(a)
    }
    
  2. 请使用内置的sort包对数组var a = [...]int{3, 7, 8, 9, 1}进行排序

    package main
    
    import (
    "fmt"
    "sort"
    )
    
    func main() {
        var a = make([] int,5,10)
        fmt.Println(a)
        for i:=0;i<10;i++{
    	    a = append(a, i)
        }
        fmt.Println(a)
    
        var a1 = [...]int{3,7,8,9,1}
        sort.Ints(a1[:])   // 对切片进行排序
        fmt.Println(a1)
    }
    

本文参考:
https://www.liwenzhou.com/posts/Go/05_array/
https://www.liwenzhou.com/posts/Go/06_slice/

原文地址:https://www.cnblogs.com/huiyichanmian/p/12753179.html