Go语言基础之5--数组(array)和切片(slince)
一、数组(array)
1.1 数组定义
1)含义:
数组是同一类型的元素集合。
数组是具有固定长度并拥有零个或者多个相同数据类型元素的序列。
2)定义一个数组的方法:
var 变量名[len] type
例子:
var a[5]int //5个整数(int)类型的数组
var a[5]string //5个字符串(string)类型的数组
解释:
像上面这种定义方法,我们是指定了数组的长度,但是还有如下定义方法:
var a=[...]int{1,2,3}
如果把数组的长度替换为...,那么数组的长度由初始化数组的元素个数决定;
3)数组中的每个元素是通过索引来访问,索引是从0开始
Go中数组下标从0开始,因此长度为n的数组下标范围: [0,n-1]
例如 数组var a[5]int 获取第一个元素就是a[0],
4)获取数组的长度是通过len(a);
5)这里需要知道:数组的长度也是数组类型的一部分,所以要知道[3]int和[4]int是不同的数组类型;
6)默认情况下一个新数组中的元素初始值为元素类型的零值;
整数数组中的元素默认初始化为0,字符串数组中的元素默认初始化为" "
如一个整数(int)类型的数组,默认值就是0;
7)数组时值类型,每次传递都将产生一份副本
8)数组不能自动扩容,数组长度在定义后就不可更改。
1.2 初始化数组
go语言中,如果数组不进行初始化的话,那么数组的元素则为默认值,int为0,sting为" "
方法1:
var a [3]int a[0] = 10 a[1] = 20 a[2] = 30
方法2:
var a [3]int = [3]int{10, 20, 30}
方法3:
a := [3]int{10, 20, 30}
方法4:
a := […]int{10, 20, 30}
补充:
当数组元素较多时,我们可以将数组长度写为... Go编译器会自动帮我们数好填上去。
方法5:
a := [3]int{10}
补充:
数组长度为3,初始化时数组只有1个元素。所以a[0]=10,a[1]和a[2]都等于默认值0
方法6:
a := [3]int{2:10}
补充:
数组也相当于一个特殊的字典,所以我们也可以在定义数组时,以字典形式进行定义,此例的意思就是a[2]=10,而a[0]和a[1]因为没定义,所以均为默认值0
1.3 数组长度是类型的一部分
var a [3]int a[0] = 10 a[1] = 20 a[2] = 30 var b [5]int b = a //因为a和b长度不一样,所以a、 b是不同类型的数组,不能赋值,赋值会溢出的。
1.4 len内置函数
package main import ( "fmt" ) func main() { var a [3]int a[0] = 10 a[1] = 20 a[2] = 30 fmt.Printf("len:%d\n", len(a)) }
执行结果:
1.5 数组遍历
方法1:
package main import ( "fmt" ) func main() { var a [3]int a[0] = 10 a[1] = 20 a[2] = 30 for i := 0; i < len(a); i++ { fmt.Printf("a[%d]=%d\n", i, a[i]) } }
执行结果:
方法2:(推荐)
package main import ( "fmt" ) func main() { var a [3]int a[0] = 10 a[1] = 20 a[2] = 30 for index, val := range a { fmt.Printf("index:%d, value:%d\n", index, val) } }
执行结果:
解释:
该种方法使用了range关键字,range是一个内置关键字,当我们用range遍历数组时,range会返回索引值和元素。这里index和val两个变量被range赋值,index代表的就是数组下标值也就是索引值,val代表的是元素值,也就是数组下标对应的结果。
1.6 二维数组
二维数组本质上是一维数组的列表。要声明一个大小为x,y的二维整数数组,可以这样写:
其中variable_type
可以是任何有效的Go数据类型,arrayName
将是有效的Go标识符。 二维数组可以被认为是具有x
个行和y
个列的表。包含三行四列的二维数组a
可以如下所示:
因此,数组a
中的每个元素由形式a[i][j]
的元素名称标识,其中a
是数组的名称,i
和j
是唯一标识a
数组中的每个元素的下标。
实例1-1 实例1
package main import ( "fmt" ) func main() { var a [3][2]int = [3][2]int{ //初始化二维数组 {1, 2}, {2, 3}, {3, 4}, } for index, row := range a { fmt.Printf("row:%d value:%v\n", index, row) } for index, row := range a { ////根据行遍历出来的数组,index就是行数,row就是值 for cal, value := range row { //想要将列也显示出来,那么我们就在做一个for循环将外层for循环遍历的数组的值在遍历一下即可。 fmt.Printf("a[%d][%d]=%d\n", index, cal, value) //index是行数,cal是列数,value是对应的值 } } }
执行结果如下:
实例1-2 实例2
package main import ( "fmt" ) func printarray(a [3][2]string) { for _, v1 := range a { for _, v2 := range v1 { fmt.Printf("%s ", v2) } fmt.Printf("\n") } } func main() { a := [3][2]string{ {"lion", "tiger"}, {"cat", "dog"}, {"pigeon", "peacock"}, } printarray(a) var b [3][2]string b[0][0] = "apple" b[0][1] = "samsung" b[1][0] = "microsoft" b[1][1] = "google" b[2][0] = "AT&T" b[2][1] = "T-Mobile" fmt.Printf("\n") printarray(b) }
执行结果如下:
1.7 数组的拷贝和传参
数组的拷贝:
首先我们要知道数组是值类型,同样类型、同样长度数组,我们将其互相赋值,被赋值的相当于将数组中的每一个元素拷贝一份,说明数组时值类型。具体见如下实例:
package main import ( "fmt" ) func main() { var a [3]int a[0] = 10 a[1] = 20 a[2] = 30 b := a //b拷贝了数组a中所有元素 b[0] = 1000 fmt.Println(a, b) }
执行结果如下:
解释:
我们可以发现将数组a赋值给变量b,其实变量b就相当于拷贝数组a中所有元素,为什么这么说呢?下面我们将b[0]赋值为1000,此时a[0]的结果依然还是10,是没有改变的,这也就验证了数组时值类型。
数组的传参:
因为数组是值类型,函数传参也会拷贝(因为是值类型,传参是相当于要把数组中的所有元素都要拷贝一份)。
具体见如下实例:
package main import ( "fmt" ) func main() { var a [3]int a[0] = 10 a[1] = 20 a[2] = 30 modify(a) fmt.Println(a) } func modify(b [3]int) { b[0] = 1000 return }
执行结果:
解释:
上述示例modify这个函数,待传入的参数类型要求是数组,我们将数组a传进去,b就相当于a的拷贝,我们修改b,是不会影响a的,因为他们的内存地址是相互独立的,两者仅仅只是做了元素的值的拷贝。
补充:
有一个问题,当我们数组元素个数较大时,当我们进行拷贝或传参时,其的底层就相当于去拷贝一份,这样会严重影响性能,如何解决呢?
答:我们可以传内存地址,一个内存地址的话32位系统4字节,64位系统8字节。
二、 切片(slince)
2.1 切片定义
1)切片是基于数组类型做的一层封装。它非常灵活,可以自动扩容。
2)slice 表示一个拥有相同类型元素的可变长的序列
3)和数组对比slice似乎就是一个没有长度的数组([]中写了长度就是数组,没写长度就是切片)
4)切片是引用类型。(切片是在数组基础上做的,实际上是指向数组的,修改切片的值,实际也修改了切片指向的数组中的值,)
5)牢记一点:切片底层就是指向数组,基于数组的。
6)创建切片有2种方法,第一种:在已有数组基础上做切片;第二种:使用make来做;
7)切片默认值:如果类型是int,默认值为0;如果类型是string,默认值是空字符串
var a []int //定义一个int类型的空切片
实例2-1
package main import ( "fmt" ) func main() { var a []int //空切片 var b [6]int = [6]int{1, 2, 3, 4, 5, 6} //初始化数组b a = b[0:2] //基于数组b创建一个切片,包括第0个元素和第1个元素 fmt.Printf("a=%v\n", a) //包括第0个元素一直到最后一个元素 a = b[0:] fmt.Printf("a=%v\n", a) //包括第2个元素一直到最后一个元素 a = b[2:] fmt.Printf("a=%v\n", a) //包括第2个元素一直到最后一个元素 a = b[:2] fmt.Printf("a=%v\n", a) //包括第0个元素一直到最后一个元素 a = b[:] fmt.Printf("a=%v\n", a) }
执行结果:
2.2 初始化切片
2.2.1 初始化方法1
切片初始化, a[start:end]创建一个包括从start到end-1的切片。
package main import ( "fmt" ) func main() { a := [5]int{76, 77, 78, 79, 80} //先生成一个数组 var b []int = a[1:4] //基于数组a创建一个切片,包括元素a[1] a[2] a[3] fmt.Println(b) }
执行结果:
2.2.2 初始化方法2
package main import ( "fmt" ) func main() { c := []int{6, 7, 8} //创建一个数组并返回一个切片(go语言在底层创建的依然是一个数组,只不过这个数组我们看不见,返回给我们的是一个切片) fmt.Println(c) }
执行结果:
2.3 数组切片的基本操作
a) arr[start:end]:包括start到end-1(包括end-1)之间的所有元素
b) arr[start:]:包括start到arr最后一个元素(包括最后一个元素)之间的所有元素
c) arr[:end]:包括0到end-1(包括end-1)之间的所有元素
d) arr[:]:包括整个数组的所有元素
2.4 切片修改
注意:切片是引用类型。(切片是在数组基础上做的,实际上是指向数组的,修改切片的值,实际也修改了切片指向的数组中的值,)
实例2-2
实例1(基础实例)
package main import ( "fmt" ) func main() { var a []int var b [6]int = [6]int{1, 2, 3, 4, 5, 6} //包括第0个元素,第1个元素 a = b[0:2] fmt.Printf("a=%v\n", a) a[0] = 1000 //修改切片 fmt.Printf("%v\n", b) }
执行结果:
解释:
我们可以发现a[0]原本是等于1的,但是后来我们修改了a[0]为1000,输出数组b的结果后,我们发现b[0]也变为了1000,好多人很疑惑,改的是切片,为什么数组的结果也变了,其实很好解释,因为切片是引用类型,所以b[0]也变了。
实例2-3
package main import ( "fmt" ) func main() { //创建一个数组,其中[…]是编译器确定数组的长度,darr的长度是9 darr := [...]int{57, 89, 90, 82, 100, 78, 67, 69, 59} //基于darr创建一个切片dslice,包括darr[2],darr[3],darr[4]三个元素 dslice := darr[2:5] fmt.Println("array before", darr) for i := range dslice { //对于dslice中每个元素进行+1,其实修改是darr[2],darr[3],darr[4] dslice[i]++ } fmt.Println("array after", darr) }
执行结果:
解释:
for i := range dslice 其实就是for i,_ := range dslice的简写。
这里range返回的是切片dslice的元素值
dslice[i]++ 表示切片的元素值加1
实例2-4
package main import ( "fmt" ) func main() { numa := [3]int{78, 79, 80} //创建一个切片,包含整个数组的所有元素 nums1 := numa[:] nums2 := numa[:] fmt.Println("array before change 1", numa) nums1[0] = 100 fmt.Println("array after modification to slice nums1", numa) nums2[1] = 101 fmt.Println("array after modification to slice nums2", numa) }
执行结果如下:
2.5 使用make创建切片
在利用make创建切片之前,我们可以发现我们都是基于已有数组做的切片,这是其中一种方法,另一种方法就是我们接下来要用的make(make是一个内置函数)创建切片了(其也是基于数组,只不过是底层已经创建好了,我们看不见),比较推荐这种,应用范围也比较广。切片元素的默认值为0。
make在这里做的就是在底层做了一个数组并且分配内存空间,切片然后去引用这个数组。
实例2-5
package main import ( "fmt" ) func main() { i := make([]int, 5, 10) //[]中没有长度就是切片,[]int,5 ,10就是告诉make函数要创建一个类型为int,长度为5(也就是切片中有5个元素),容量为10的切片。 //长度表示实际占的长度(也就是已经使用了的量),容量表示底层我们的切片整体一共有多少资源。 fmt.Println(i) }
执行结果如下:
2.6 切片的长度和容量
精华总结
关于创建切片时长度和容量的概念:
1)形象点例子:教师里一共有10个座位,并且10个都是空座位,此时我们基于此基础建立切片,在坐了5个之后,那么此时长度就是5(切片的元素也就是5个),容量还是10,容量是不会变的,除非扩容。
2)但是另一种情况:教师里一共有10个座位,如果我们在建立切片之前已经有1个座位被坐了,再此基础上,我们在建立切片,此时容量就是9了,如果在坐5个座位,那么此时长度为5(切片元素为5),容量为9.
3)容量其实就是指的底层数组的可以使用(因为有时基于数组创建切片时下标不是0,而是1,那么对应的a[0]就相当于已经使用了,不算做可以使用的切片资源了,相应的容量也会少一个)的长度。
4)切片基于数组创建,容量就是在创建切片后,根据创建切片的下标,可以使用的数组量,而长度就是在创建切片后在已有容量中使用了的量。
在go语言中,我们可以使用len函数来获取切片长度,cap来获取切片的容量。
实例2-6
package main import ( "fmt" ) func main() { fruitarray := [...]string{ "apple", "orange", "grape", "mango", "water melon", "pine apple", "chikoo"} fruitslice := fruitarray[1:3] //长度是2,容量is 6 fmt.Printf("length of slice %d capacity %d", len(fruitslice), cap(fruitslice)) }
执行结果如下:
2.7 切片的再切片
实例2-7 实例1
package main import ( "fmt" ) func main() { var a []int var b [6]int = [6]int{1, 2, 3, 4, 5, 6} a = b[0:2] //包括第0个元素、第1个元素 fmt.Printf("len(a):%d cap:%d\n", len(a), cap(a)) //打印出来长度为2,容量为6(因为切片指向的是底层数组,底层数组的长度为6,所以切片的容量也是为6) a = b[2:6] fmt.Printf("len(a):%d cap:%d\n", len(a), cap(a)) //打印出来长度为4,容量为4(因为元素是从2开始的前面的0和1已经占用了(前面的元素不能在引用了),不能在用了,所以容量为4) //接下来做切片的再切片 fmt.Println(a) //做在切片之前切片a现在包含的元素值 c := a[1:2] //现在要取的就是第一个元素 fmt.Printf("c=%v\n", c) }
执行结果如下:
实例2-8 实例2
package main import ( "fmt" ) func main() { fruitarray := [...]string{ "apple", "orange", "grape", "mango", "water melon", "pine apple", "chikoo"} fruitslice := fruitarray[1:3] //长度是2, 容量是6 fmt.Printf("length of slice %d capacity %d\n", len(fruitslice), cap(fruitslice)) //再重新进行切片,不能大于数组fruitarray的长度,否则越界 fruitslice = fruitslice[:cap(fruitslice)] fmt.Println("After re-slicing length is", len(fruitslice), "and capacity is", cap(fruitslice)) }
执行结果:
2.8 append操作
append就是给切片扩容
实例2-9 实例1
package main import ( "fmt" ) func main() { var a []int a = make([]int, 2, 2) fmt.Printf("%v len:%d, cap:%d, addr:%p\n", a, len(a), cap(a), a) a = append(a, 100) fmt.Printf("%v len:%d, cap:%d, addr:%p\n", a, len(a), cap(a), a) a = append(a, 100, 200, 300, 400) fmt.Printf("%v len:%d, cap:%d, addr:%p\n", a, len(a), cap(a), a) }
执行结果:
解释:
容量不够就会扩容,扩容后,我们可以发现内存地址已经变化了(形象点说就是将原来内存地址的东西现在搬到新的大的内存地址中),在1024以下容量是翻倍扩容,1024以上的话是有一个系数的,就不是翻倍扩容了。
实例2-10 实例2
package main import ( "fmt" ) func main() { var a []int = make([]int, 5, 5) //用make创建了一个长度为5,容量也为5的切片 b := a //切片a和b都同时指向底层长度为5的数组 fmt.Printf("a = %v addr:%p\n", a, a) fmt.Printf("b = %v addr:%p\n", b, b) a = append(a, 10) //现在扩容a切片 fmt.Printf("a = %v addr:%p\n", a, a) fmt.Printf("b = %v addr:%p\n", b, b) }
执行结果:
解释:(重要)
此案例是,在a切片未扩容之前,切片a和b都是指向底层同一个数组,但是a切片扩容后,a切片指向的底层数组的内存地址变化了(做的操作就是将原来内存地址数组的东西拷贝放到新的内存地址中形成新的大数组),而切片b是没有扩容的,其依然指向原来数组的内存地址。
实例2-11 实例3
package main import ( "fmt" ) func main() { cars := []string{"Ferrari", "Honda", "Ford"} //长度和容量都等于3 fmt.Println("cars:", cars, "has old length", len(cars), "and capacity", cap(cars)) cars = append(cars, "Toyota") //容量等于6 fmt.Println("cars:", cars, "has new length", len(cars), "and capacity", cap(cars)) }
执行结果:
思考:
为什么append切片操作,我们还要为其赋值呢?
比如说正常为:a = append(a, 5) 为什么要这样写呢?
解答:我们知道切片是引用类型。不涉及值类型更改不生效的问题。主要是因为一个原因。切片扩容时,原有切片内存地址已经改变了,所以我们在用append操作时,还需要赋值给切片。如果不扩容的话,那么直接append(a,5)就可以的,但是我们不论要不要扩容,肯定是要考虑到扩容的情况,必须进行切片赋值的。
2.9 空切片
1) 如何判断一个切片的值(引用类型)为空呢?我们可以用一个关键字nil(底层就是一个空内存地址0x0)来进行判断,并且可以使用append关键字来为其扩容。
2) 空切片一定要初始化,如果不初始化,直接访问就会报错,但是append不会报错,append除外,append会自动对这个空切片做自动扩容。
注意:空切片就是长度容量都等于0。切记不要用长度等于0来判断是否为空切片,因为长度等于0,容量不等于0,其就不是空切片了。
实例2-12 实例1
package main import ( "fmt" ) func main() { //定义names是一个空切片,长度和容量都等于0 //不能对空切片进行访问,否则panic var names []string if names == nil { //nil来进行判断切片是否为空切片 fmt.Println("slice is nil going to append") names = append(names, "John", "Sebastian", "Vinay") //在空切片基础上,为切片append一些值 fmt.Println("names contents:", names) } }
执行结果如下:
实例2-13 实例2
package main import ( "fmt" ) func main() { var a []int if a == nil { fmt.Printf("a is null addr\n") } // a[0]=100 //不能这么写,这样就越界了。因为a是空切片,会报错(panic) fmt.Printf("a:%v addr:%p\n", a, a) //打印空切片地址 a = append(a, 2, 3, 3) //append可以直接针对空切片进行赋值扩容 fmt.Printf("a:%v addr:%p\n", a, a) }
执行结果如下:
2.10 append一个切片
append一个切片就相当于在要追加的切片变量后面加三个点,就相当于把切片变成了一个个元素,就相当于展开了。
实例2-14 实例1
package main import ( "fmt" ) func main() { var a []int //定义一个空切片a var b []int = []int{1, 2, 3} a = append(a, b...) //再要追加的切片变量后面加三个点,就相当于把切片变成了一个个元素,就相当于展开了。 //把切片b展开成一个个元素 fmt.Printf("append slince:%v\n", a) }
执行结果如下:
实例2-15 实例2
package main import ( "fmt" ) func main() { veggies := []string{"potatoes", "tomatoes", "brinjal"} fruits := []string{"oranges", "apples"} //fruits后面的3个点表示展开fruits切片成一个个元素 food := append(veggies, fruits...) fmt.Println("food:", food) }
执行结果如下:
2.11 切片传参
实例2-16 实例1
package main import ( "fmt" ) //在函数内部修改numbers切片的值 func subtactOne(numbers []int) { for i := range numbers { numbers[i] -= 2 } } func main() { nos := []int{8, 7, 6} fmt.Println("slice before function call", nos) subtactOne(nos) //nos修改生效了,说明切片是引用类型 fmt.Println("slice after function call", nos) }
执行结果如下:
2.12 切片拷贝
copy函数:
copy(a,b) 相当于将b的元素拷贝到a对应的位置。
实例2-17 实例1
package main import ( "fmt" ) func main() { veggies := []string{"potatoes", "tomatoes", "brinjal"} fruits := []string{"oranges", "apples"} copy(fruits, veggies) //copy内置函数相当于把一个切片的内容拷贝到另外一个切片中。 //相当于把第二个切片的的元素拷贝到第一个切片对应位置,谨记。 fmt.Println(veggies, fruits) }
执行结果如下:
实例2-18 实例2
package main import ( "fmt" ) func main() { a := []int{1, 2, 3} b := []int{9, 8} copy(b, a) fmt.Printf("a:%v b:%v", a, b) }
执行结果如下:
2.13 切片遍历
两种方法,和数组一样。
方法1:for range (推荐)
package main import ( "fmt" ) func main() { var a [3]int a[0] = 10 a[1] = 20 a[2] = 30 b := a[:] for index, val := range b { fmt.Printf("index:%d value:%d\n", index, val) } }
执行结果如下:
方法2:
package main import ( "fmt" ) func main() { var a [3]int a[0] = 10 a[1] = 20 a[2] = 30 b := a[:] for i := 0; i < len(b); i++ { fmt.Printf("b[%d]=%d\n", i, b[i]) } }
执行结果如下:
2.14 切片扩容
切片扩容实际上就是:
切片本来的容量是4,现在容量不够了,切片内部自动扩容,比如说扩容到8,其在底层的机制就是将旧的内存地址中的4个元素拷贝到新到容量为8的内存地址中,然后再继续接收新元素并使用。
三、make和new的区别
之后待补充