• Go语言学习之路第4天(数组和切片)


    一.数组

      如果要存储班级里所有学生的数学成绩,应该怎样存储呢?可能有同学说,通过定义变量来存储。但是,问题是班级有80个学生,那么要定义80个变量吗?

      像以上情况,最好是通过数组的方式来存储。

      所谓的数组:是指一系列同一类型数据的集合。

    1.1 数组定义

      数组定义的格式为:var 数组名 [元素个数]数据类型,

      例如:var a [10]int;数组定义也是通过var 关键字,后面是数组的名字a,长度是10,数据类型是整型。表示:数组a能够存储10个整型数字。也就是说,数组a的长度是10。

      我们可以通过len( )函数测试数组的长度,如下所示:

    func main(){
    	var a [10]int
    	fmt.Println(len(a))
    
    }
    

      当定义完成数组a后,就在内存中开辟了10个连续的存储空间,每个数据都存储在相应的空间内,数组中包含的每个数据被称为数组元素(element),一个数组包含的元素个数被称为数组的长度。

      注意:数组的长度只能是常量。以下定义是错误的:

    var n int = 10
    var a [n]int
    

    1.2 数组赋值

      数组定义完成后,可以对数组进行赋值操作。

      数组是通过下标来进行操作的,下标的范围是从0开始到数组长度减1的位置。

     

      var a[10] int   表示的范围是a[0],a[1],a[2].......,a[9]

     

      完成对数组赋值的第一种方法:在定义完数组后,通过数组下标为数组中的元素赋值

    func main(){
    	var a [10]int
    	a[0] = 1
    	a[1] = 2
    	a[2] = 3
    	a[3] = 4
    	a[4] = 5
    	a[5] = 6
    	a[6] = 7
    	a[7] = 8
    	a[8] = 9
    	a[9] = 10
    	fmt.Println(a[0])
    	fmt.Println(a[9])
    }
    

      但如果现在给a[10]=29, 会出现什么情况呢?

    func main(){
    	var a [10]int
    	a[10] = 29
    }
    

      结果如下:

    invalid array index 10 (out of bounds for 10-element array)
    

      如果通过数组中不存在的下标赋值,就会报数组下标越界的错误,这里在使用的时候要注意。

      但是上面赋值方式比较麻烦,所以可以使用第二种赋值方式,如下所示:

    func main(){
    	var a [10]int
    	for i := 0;i < 10;i++{
    		a[i] = i + 1
    	}
    	for i := 0;i < 10;i++{
    		fmt.Println(a[i])
    	}
    }
    

      通过for循环完成数组的赋值与输出。注意:循环的条件,如果将循环条件修改成i<=10是否正确

      在上一节中,我们说过可以通过len( )函数来获取 数组的长度,所以也可以对上面的程序,进行如下的修改

    func main(){
    	var a [10]int
    	for i := 0;i < len(a);i++{
    		a[i] = i + 1
    	}
    	for i := 0;i < len(a);i++{
    		fmt.Println(a[i])
    	}
    }
    

      对数组中的数据输出,也可以使用range.如下所示:

    func main(){
    	var a [10]int
    	for i := 0;i < len(a);i++{
    		a[i] = i + 1
    	}
    	for i,data := range a{
    		//fmt.Println("下标:",i)
    		//fmt.Println("元素值:",data)
    		fmt.Printf("a[%d]=%d
    ",i,data)
    	}
    }
    

      i变量存储的是数组的下标,data变量存储的是数组中的值。

      如果只想输出数组中的元素值,不希望输出下标,可以使用匿名变量

    func main(){
    	var a [10]int
    	for i := 0;i < len(a);i++{
    		a[i] = i + 1
    	}
    	for _,data := range a{
    		//fmt.Println("下标:",i)
    		fmt.Println("元素值:",data)
    		//fmt.Printf("a[%d]=%d
    ",i,data)
    	}
    }
    

     

      上面的案例中,首先完成了数组的赋值,然后再输出数组中的值。但是,如果定义完成数组后,没有赋值,直接输出会出现什么样的问题呢?

    func main(){
    	var a [10]int
    	for i := 0;i < len(a);i++{
    		fmt.Println(a[i])
    	}
    }
    

      a数组中的元素类型是整型,定义完成后,直接输出,结果全部是0.

      当然数组中存储的元素类型也可以是其它类型,如下所示:

      var a [10]float64//如果不赋值,直接输出,结果默认全部是0

      vara[10]string//如果不赋值,直接输出,结果默认全部是空字符

      var a [10]bool//如果不赋值,直接输出,结果默认全部是false.


    1.3 数组初始化

      上一小节中,首先先定义数组,然后再完成数组的赋值。其实,在定义数组时,也可以完成赋值,这种情况叫做数组的初始化。

    func main(){
    	//定义数组 并为数组中所有元素赋值
    	var arr1 [10]int = [10]int{1,2,3,4,5,6,7,8,9,0}
    
    	//通过自动推导类型定义并初始化数组
    	arr2 := [5]int{1,2,3,4,5}
    
    	//定义数组为部分数据赋值 未赋值的数据默认值为0
    	var arr3 [10]int = [10]int{1,2,3,4,5}
    
    	//通过初始化,来确定数组长度
    	arr4 := [...]int{1,2,3,4}
    
    	fmt.Println(arr1)
    	fmt.Println(arr2)
    	fmt.Println(arr3)
    	fmt.Println(len(arr4))
    }
    

      结果如下:

    [1 2 3 4 5 6 7 8 9 0]
    [1 2 3 4 5]
    [1 2 3 4 5 0 0 0 0 0]
    4
    

    1.4 数组的内存存储 

      先看下面这个例子:

    func main(){
    	var arr1 [10]int = [10]int{1,2,3,4,5,6,7,8,9,0}
    	//%p  是一个占位符 表示输出一个数据的内存地址
    	fmt.Printf("%p
    ",&arr1)
    	for i := 0;i < len(arr1);i++{
    		fmt.Printf("%p
    ",&arr1[i])
    	}
    }
    

      结果如下:

    0xc00006a050
    0xc00006a050
    0xc00006a058
    0xc00006a060
    0xc00006a068
    0xc00006a070
    0xc00006a078
    0xc00006a080
    0xc00006a088
    0xc00006a090
    0xc00006a098
    

      从代码结果可以看出,数组的内存地址其实就是数组中首元素的地址;另外由于Go语言中,整形在内存中占8个字节,由此可以确定数组中的数据是在内存中连续存储的。

    1.5 数组练习

      从一个整数数组中取出最大的整数,最小整数,总和,平均值。

      代码如下:

    func main(){
    	score := [10]int{89, 72, 86, 91, 74, 83, 94, 0, 100, 60}
    
    	var max int
    	var min int
    	var sum int
    
    	for i := 0;i < len(score);i++{
    		if score[i] > max{
    			max = score[i]
    		}
    		if score[i] < min{
    			min = score[i]
    		}
    		sum += score[i]
    	}
    	fmt.Printf("最大值是:%d,最小值是:%d,和为:%d,平均值为:%.2f",max,min,sum,float64(sum)/float64(len(score)))
    }
    

      以上程序输出的结果是:

    最大值是:100,最小值是:0,和为:749,平均值为:74.90
    

      通过观察发现该程序输出的结果没有问题。

      但是,现在将程序进行如下修改:将数组中的0元素删除,换成其他不为0的正整数

    func main(){
    	score := [10]int{89, 72, 86, 91, 74, 83, 94, 78, 100, 60}
    
    	var max int
    	var min int
    	var sum int
    
    	for i := 0;i < len(score);i++{
    		if score[i] > max{
    			max = score[i]
    		}
    		if score[i] < min{
    			min = score[i]
    		}
    		sum += score[i]
    	}
    	fmt.Printf("最大值是:%d,最小值是:%d,和为:%d,平均值为:%.2f",max,min,sum,float64(sum)/float64(len(score)))
    }
    

      运行以上程序,结果如下:

    最大值是:100,最小值是:0,和为:827,平均值为:82.70
    

      思考:数组中没有0,为什么输出的结果中最小值为0呢?

      现在,在将程序进行如下修改:将数组中的数据全部修改成负数

    func main(){
    	//score := [10]int{89, 72, 86, 91, 74, 83, 94, 78, 100, 60}
    	score := [5]int{-1,-2,-3,-4,-5}
    	var max int
    	var min int
    	var sum int
    
    	for i := 0;i < len(score);i++{
    		if score[i] > max{
    			max = score[i]
    		}
    		if score[i] < min{
    			min = score[i]
    		}
    		sum += score[i]
    	}
    	fmt.Printf("最大值是:%d,最小值是:%d,和为:%d,平均值为:%.2f",max,min,sum,float64(sum)/float64(len(score)))
    }
    

      运行该程序,结果输出:

    最大值是:0,最小值是:-5,和为:-15,平均值为:-3.00
    

      

      思考:数组中没有0,为什么输出的结果中最大值为0呢?(整数类型默认值是0)

      应该怎样解决如上的问题呢?将程序修改如下:

    func main(){
    	score := [10]int{89, 72, 86, 91, 74, 83, 94, 78, 100, 60}
    
    	var max int = score[0]
    	var min int = score[0]
    	var sum int
    
    	for i := 0;i < len(score);i++{
    		if score[i] > max{
    			max = score[i]
    		}
    		if score[i] < min{
    			min = score[i]
    		}
    		sum += score[i]
    	}
    	fmt.Printf("最大值是:%d,最小值是:%d,和为:%d,平均值为:%.2f",max,min,sum,float64(sum)/float64(len(score)))
    }
    

    1.6 数组冒泡排序 

      如何对数组中存储的数据,按照从大到小,或者从小到大进行排序?可以使用冒泡排序。

      它重复地走访过要排序的元素列,依次比较两个相邻的元素,如果他们的顺序(如从大到小、首字母从A到Z)错误就把他们交换过来。走访元素的工作是重复地进行直到没有相邻元素需要交换,也就是说该元素列已经排序完成。

      具体代码实现:

    func main(){
    	array := [10]int{8,4,2,9,1,5,7,3,6,10}
    
    	for i := 0;i < len(array) - 1;i++{
    		for j := 0;j < len(array) - 1 - i;j++{
    			if array[j] > array[j+1]{
    				array[j],array[j+1] = array[j+1],array[j]
    			}
    		}
    	}
    	fmt.Println(array)
    }
    

      结果如下:

    [1 2 3 4 5 6 7 8 9 10]
    

    1.7 数组作为函数参数

      数组也可以像变量一样,作为参数传递给函数,用法如下:

    func BubbleSort(array [10]int){
    	for i := 0;i < len(array)-1;i++{
    		for j := 0;j < len(array)-1-i;j++{
    			if array[j] > array[j+1]{
    				array[j],array[j+1] = array[j+1],array[j]
    			}
    		}
    	}
    }
    
    func main()  {
    	array := [10]int{9, 1, 5, 6, 10, 8, 3, 7, 2, 4}
    	BubbleSort(array)
    	fmt.Println(array)
    }
    

      结果如下:

    [9 1 5 6 10 8 3 7 2 4]
    

      注意:在main()函数中,定义数组array,然后调用BubbleSort()方法传递数组,同时在BubbleSort()方法中修改数组中的元素。最终输出结果发现,并不会影响main( )函数中数组array的值,这一点与其它编程语言是有区别的。如果想得到修改后的数组,可以添加返回值,具体如下:

    func BubbleSort(array [10]int) [10]int{
    	for i := 0;i < len(array)-1;i++{
    		for j := 0;j < len(array)-1-i;j++{
    			if array[j] > array[j+1]{
    				array[j],array[j+1] = array[j+1],array[j]
    			}
    		}
    	}
    	return array
    }
    
    func main()  {
    	array := [10]int{9, 1, 5, 6, 10, 8, 3, 7, 2, 4}
    	array = BubbleSort(array)
    	fmt.Println(array)
    }
    

    1.8 二维数组

       前面定义的数组只有一个下标,称之为一维数组,如果有两个下标,称之为二维数组。

      二维数组的定义如下:var 数组名 [行个数][列个数]数据类型;例如:var arr [2][3]int。

      为二维数组赋值:数组名[行下标][列下标]=值,示例如下:

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

      结果如下:

    [[1 0 0] [0 0 4]]
    

      

      二维数组的初始化:

    func main()  {
    	//定义二维数组并初始化
    	var arr1 [2][3]int = [2][3]int{{1,2,3},{4,5,6}}
    
    	//定义二维数组并初始化部分数据的值 未初始化部分 默认值为定义的数据类型的零值
    	var arr2 [2][3]int = [2][3]int{{1,2},{4}}
    
    	//自动推导类型创建二维数组
    	arr3 := [2][3]int{{1,2,3},{4,5,6}}
    
    	fmt.Println(arr1)
    	fmt.Println(arr2)
    	fmt.Println(arr3)
    }
    

      结果如下:

    [[1 2 3] [4 5 6]]
    [[1 2 0] [4 0 0]]
    [[1 2 3] [4 5 6]]
    

      

       遍历二维数组:

      len(二维数组):计算二维数组中行的个数

      len(二维数组[行下标]):计算二维数组中列的个数

    func main()  {
    	var arr [2][3]int = [2][3]int{{1,2,3},{4,5,6}}
    	fmt.Printf("此二维数组的行数为%d
    ",len(arr))
    	fmt.Printf("此二维数组的列数为%d
    ",len(arr[0]))
    
    	//遍历二维数组
    	for i := 0;i < len(arr);i++{
    		for j := 0;j < len(arr[0]);j++{
    			fmt.Printf("%d	",arr[i][j])
    		}
    		fmt.Println()
    	}
    	fmt.Println("----------------------")
    	//使用range遍历二维数组
    	for _,d := range arr{
    		for _,v := range d{
    			fmt.Printf("%d	",v)
    		}
    		fmt.Println()
    	}
    }
    

      结果如下:

    此二维数组的行数为2
    此二维数组的列数为3
    1	2	3	
    4	5	6	
    ----------------------
    1	2	3	
    4	5	6	
    

      

    二.切片

    2.1 切片概念

      在讲解切片(slice)之前,大家思考一下数组有什么问题?

      第一:数组定义完,长度是固定的。

      例如:

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

      定义的num数组长度是5,表示只能存储5个整型数字,现在向数组num中追加一个数字,这时会出错。

      第二:使用数组作为函数参数进行传递时,如果实参为5个元素的整型数组,那么形参也必须5个元素的整型数组,否则出错。

      针对以上两个问题,可以使用切片来进行解决。

      切片:切片与数组相比切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大,所以可以将切片理解成“动态数组”,但是,它不是数组。

    2.2 切片与数组的区别

      通过定义,来比较一下切片与数组的区别

      (1)先回顾数组的基本定义初始化:

    a := [5]int{1,2,3,4,5}
    

      数组中[ ]是一个固定的数字,表示长度。定义完后,长度是固定,最多存储5个数字。

      (2)切片的基本定义初始化如下:

      var 切片名 []数据类型

    //切片的定义
    var a []int = []int{1,2,3,4,5}
    //使用自动推导类型定义切片
    b := []int{1,2,3,4,5}
    

      看定义的方式,发现与数组很相似,但是注意:切片中的[ ]是空的,切片的长度和容量可以不固定。

      同时切片还可以动态的追加数据,使用append()函数。

      切片名 = append(切片名,值1,值2,值3...)

    func main()  {
    	s := []int{1,2,3,4,5}
    	s = append(s,6,7,8,9)
    	fmt.Println(s)
    }
    

      结果如下:

    [1 2 3 4 5 6 7 8 9]
    

      也可将一个切片追加到另一个切片后

      切片名 = append(切片名,切片名2...) //注意:这 ... 是必须加上的

    func main()  {
    	s := []int{1,2,3,4,5}
    	t := []int{6,7,8,9,0}
    	s = append(s,t...)
    	fmt.Println(s)
    }
    

      结果如下:

    [1 2 3 4 5 6 7 8 9 0]
    

    2.3 切片的定义和初始化

      定义并初始化切片:

    var slice []int = []int{1, 2, 3, 4, 5}
    

      

      声明切片:

    var slice []int
    

      只声明但没有初始化的切片是不能使用下标进行赋值,当你使用下标为切片赋值时,会报下标越界的错误。

    func main()  {
    	var slice []int
    	slice[0] = 123
    	fmt.Println(slice)
    }
    
    panic: runtime error: index out of range
    

      

       我们也可以通过 make() 函数定义并初始化函数:

      借助make函数, 格式 make(切片类型, 长度, 容量)

    var slice []int = make([]int, 5, 10)
    

      什么是切片的长度与容量?

      长度是已经初始化的空间(以上切片slice初始空间默认值都是0)。容量是已经开辟的空间,包括已经初始化的空间和空闲的空间。

      我们可以通过如下图来理解切片的长度与容量:

      该切片的长度是5(存有数据,注意如果没有赋值,默认值都是0),容量是10,只不过有5个空闲区域。

      即使没有给切片slice赋值,初始化的空间(长度)默认存储的数据都是0。

      演示如下:

    func main() {
    	slice := make([]int,5,10)
    	fmt.Println(slice)
    }
    

      输出结果是:

    [0 0 0 0 0]
    

      

      在使用make( )函数定义切片时,一定要注意,切片长度要小于等于容量,例如:

    slice := make([]int,10,5)
    

      这样是错误的。

      make( )函数中的容量参数是可以省略掉的,如:

    slice := make([]int,10)
    

      这时长度与容量是相等的,都是10。

      GO语言提供了相应的函数来计算切片的长度与容量。

    func main() {
    	slice := make([]int,5,10)
    	fmt.Printf("长度是:%d
    ",len(slice))
    	fmt.Printf("容量是:%d
    ",cap(slice))
    }
    

      接下来给切片slice赋值,可以通过下标的方式直接来进行赋值。如下所示:

    func main() {
    	slice := make([]int,5,10)
    	slice[0] = 1
    	slice[1] = 2
    }
    

      也可以通过循环的方式来进行赋值。

    func main() {
    	slice := make([]int,5,10)
    	for i := 0;i < len(slice);i++{
    		slice[i] = i
    	}
    }
    

      在这里一定要注意,循环结束条件是小于切片的长度,而不是容量。因为,切片的长度是指的是初始化的空间。以下方式会出现异常错误。

    for i := 0;i < cap(slice);i++{
    	slice[i] = i
    }
    

      给切片赋完值后,怎样将切片中的数据打印出来呢?

      第一种方式:直接通过下标的方式输出,例如:slice[0],slice[1].....。

      第二种方式:通过循环的方式,注意循环结束的条件,也是小于切片的长度,如下所示:

    for i := 0;i < len(slice);i++{
    	fmt.Println(slice[i])
    }
    

      或者使用range方式输出:

    for k,v := range slice{
    	fmt.Println("下标:",k)
    	fmt.Println("值:",v)
    }
    

      

    2.4 切片的内存存储

      我们先来认识一个函数:unsafe.Sizeof(数据),是用来计算数据在内存中占的字节大小,用法如下:

    func main() {
    	a := 1
    	fmt.Println(unsafe.Sizeof(a))
    }
    

      结果如下:

    8
    

      然后我们再看下面这个例子:

    func main() {
    	s1 := []int{1}
    	s2 := []int{1,2,3,4,5}
    	s3 := []int{1,2,3,4,5,6,7,8,9,10}
    	fmt.Println(unsafe.Sizeof(s1))
    	fmt.Println(unsafe.Sizeof(s2))
    	fmt.Println(unsafe.Sizeof(s3))
    }
    

      结果如下:

    24
    24
    24
    

      我们发现长度各不相同的切片在内存中占的字节都是24,这是为什么呢?

      这是因为切片其实本质上是一个结构体(这个之后会学到)。

      在Go源码中runtime包下的slice.go的文件中定义如下:

    type slice struct {
      array unsafe.Pointer
      len   int
      cap   int
    }
    

      切片包含三个属性:数组指针(指针就是地址),长度和容量,其中数组指针是指切片中数据的内存地址。

      简单的图片描述如下:

      切片名本身就是一个地址,指向切片数据的内存地址。 

    func main() {
    	slice := []int{1,2,3,4,5}
    	fmt.Printf("%p
    ",slice)
    }
    0xc00008e030
    

      我们也可以通过for循环来得到切片中每个数据的内存地址:

    func main() {
    	slice := []int{1,2,3,4,5}
    	for i := 0;i < len(slice);i++{
    		fmt.Printf("%p
    ",&slice[i])
    	}
    }
    

      结果如下:

    0xc00008e030
    0xc00008e038
    0xc00008e040
    0xc00008e048
    0xc00008e050
    

      

    2.5 切片的长度和容量

      切片与数组很大的一个区别就是:切片的长度是不固定的,可以向已经定义的切片中追加数据。并且也给大家简单的演示过通过append的函数,在原切片的末尾添加元素。

    func main() {
    	slice := []int{1,2,3}
    	slice = append(slice,4) //追加一个数
    	slice = append(slice,5,6,7)  //追加多个数
    	slice = append(slice,[]int{8,9,10}...)  //追加一个切片
    }
    

      但如果容量不够用了,该怎么办呢? 

      例如有以下切片:

      s:= make([]int, 5, 8)

      定义了切片s,长度是5,容量是8

    func main() {
    	s := make([]int, 5, 8)
    	fmt.Printf("len = %d,cap = %d
    ", len(s), cap(s))
    }
    

       结果是:

    len = 5,cap = 8
    

      并且前面我们讲解过,长度是指已经初始化的空间,现在切片s没有赋值,但是默认值为0

      验证如下所示:

    func main() {
    	s := make([]int, 5, 8)
    	fmt.Printf("len = %d,cap = %d
    ", len(s), cap(s))
    	fmt.Println(s)
    }
    

       结果是:

    len = 5,cap = 8
    [0 0 0 0 0]
    

      现在开始通过append函数追加数据,如下所示:

    func main() {
    	s := make([]int, 5, 8)
    	s = append(s, 1)
    	fmt.Printf("len = %d,cap = %d
    ", len(s), cap(s))
    	fmt.Println(s)
    }
    

       输入结果是:

    len = 6,cap = 8
    [0 0 0 0 0 1]
    

      从输出的结果上,我们完全能够体会到,append函数的作用是在末尾追加(直接在默认值后面追加数据),由于追加了一个元素,所以长度为6.

      但是如果我们把程序修改成如下所示:

    func main() {
    	s := make([]int, 5, 8)
    	s[0] = 1
    	fmt.Printf("len = %d,cap = %d
    ", len(s), cap(s))
    	fmt.Println(s)
    }
    

      输出结果是:

    len = 5,cap = 8
    [1 0 0 0 0]
    

      由于s[0]=1是直接给下标为0的元素赋值,并不是追加,所以结果的长度不变。

      下面我们继续通过append( )继续追加数据:

    func main() {
    	s := make([]int, 5, 8)
    	s = append(s,1)
    	s = append(s,2)
    	s = append(s,3)
    	fmt.Printf("len = %d,cap = %d
    ", len(s), cap(s))
    	fmt.Println(s)
    }
    

       输出结果是:

    len = 8,cap = 8
    [0 0 0 0 0 1 2 3]
    

      追加完成3个数据后,长度变为了8,与容量相同。

      那么如果现在通过append( )函数,继续向切片s中继续追加一个数据,那么容量会变为多少呢?

      代码如下:

    func main() {
    	s := make([]int, 5, 8)
    	s = append(s, 1)
    	s = append(s, 2)
    	s = append(s, 3)
    	s = append(s, 4)
    	fmt.Printf("len = %d,cap = %d
    ", len(s), cap(s))
    	fmt.Println(s)
    }
    

       输出结果是:

    len = 9,cap = 16
    [0 0 0 0 0 1 2 3 4]
    

      追加完成一个数据后,长度变为9,大于创建切片s时的容量,所以切片s自动扩容,变为16.

      那么切片的容量是否是以2倍容量来进行扩容的呢?

      我们可以来验证一下:

    func main() {
    	s := make([]int, 0, 1)
    	oldCap := cap(s)
    	for i := 0; i < 20; i++ {
    		s = append(s, i)
    		newCap := cap(s)
    		if oldCap < newCap {
    			fmt.Printf("cap: %d ====> %d
    ", oldCap, newCap)
    			oldCap = newCap
    		}
    	}
    }
    

      输出结果是:

    cap: 1 ====> 2
    cap: 2 ====> 4
    cap: 4 ====> 8
    cap: 8 ====> 16
    cap: 16 ====> 32
    

      通过以上结果分析,发现是2倍的容量进行扩容。

      但是我们修改一下循环条件看一下结果,将循环结束的条件修改的大一些,如下所示:

    func main() {
    	s := make([]int, 0, 1)
    	oldCap := cap(s)
    	for i := 0; i < 20000; i++ {
    		s = append(s, i)
    		newCap := cap(s)
    		if oldCap < newCap {
    			fmt.Printf("cap: %d ====> %d
    ", oldCap, newCap)
    			oldCap = newCap
    		}
    	}
    }
    

      对应的结果:

    cap: 1 ====> 2
    cap: 2 ====> 4
    cap: 4 ====> 8
    cap: 8 ====> 16
    cap: 16 ====> 32
    cap: 32 ====> 64
    cap: 64 ====> 128
    cap: 128 ====> 256
    cap: 256 ====> 512
    cap: 512 ====> 1024  //2倍扩容
    cap: 1024 ====> 1280 //非2倍扩容
    cap: 1280 ====> 1696
    cap: 1696 ====> 2304
    cap: 2304 ====> 3072
    

      通过以上的运行结果分析:

      如果数据小于1024,每次扩容为上次的两倍;如果超过1024,扩容为上次的1/4 - 1/3

    2.6 切片的截取

      所谓截取就是从切片中获取指定的数据。

      我们通过如下程序给大家解释一下:

    func main() {
    	//定义切片并初始化
    	s := []int{10, 20, 30, 0, 0}
    
    	//从切片s中截取数据
    	slice := s[0:3:5]
    	fmt.Println(slice)
    }
    

      以上程序输出结果:

    [10 20 30]
    

      s[0:3:5]是什么意思呢?

      我们可以使用s[low:high:max]来表示

      第一个数(low)表示下标的起点(从该位置开始截取),如果low取值为0表示从第一个元素开始截取,也就是对应的切片s中的10

      第二个数(high)表示取到哪结束,也就是下标的终点(但不包含该位置),3表示取出下标是0,1,2的数据(10,20,30),不包括下标为3的数据,那么也就是说取出的数据长度是3. 可以根据公式:3-0 计算(len=high-low),也就是第二个数减去第一个数,差就是数据长度。在这里可以将长度理解成取出的数据的个数。

      第三个数用来计算容量,所谓容量:是指切片目前可容纳的最多元素个数。通过公式5-0计算(cap=max-low),也就是第三个数据减去第一个数。该案例中容量为5。

     

      将以上程序进行修改:

    func main() {
    	//定义切片并初始化
    	s := []int{10, 20, 30, 40, 50}
    
    	//从切片s中截取数据
    	slice := s[0:3:5]
    	fmt.Println(slice)
    }
    

      结果如下:

    [10 20 30]
    

      因为起点还是0,终点还是3.长度是3,容量是5。

      继续修改该程序:

    func main() {
    	//定义切片并初始化
    	s := []int{10, 20, 30, 40, 50}
    
    	//从切片s中截取数据
    	slice := s[0:4:5]
    	fmt.Println(slice)
    }
    

       结果如下:

    [10 20 30 40]
    

      继续修改该程序:

    func main() {
    	//定义切片并初始化
    	s := []int{10, 20, 30, 40, 50}
    
    	//从切片s中截取数据
    	slice := s[1:4:5]
    	fmt.Println(slice)
    }
    

      结果如下:

    [20 30 40]
    

      那么容量是多少呢?容量为4,通过第三个数减去第一个数(5-1)计算。

      通过画图的方式来表示slice切片中的容量。 

      通过上面的图,可以发现切片s经过截取操作以后,将结果赋值给切片slice后,长度是3,容量是4,只不过有一块区域是空闲的。

      关于切片的截取还有其它的操作,如下图所示:

    操作

    含义

    s[n]

    切片s中索引位置为n的项

    s[:]

    从切片s的索引位置0到len(s)-1处所获得的切片

    s[low:]

    从切片s的索引位置low到len(s)-1处所获得的切片

    s[:high]

    从切片s的索引位置0到high处所获得的切片,len=high

    s[low:high]

    从切片s的索引位置low到high处所获得的切片,len=high-low

    s[low:high:max]

    从切片s的索引位置low到high处所获得的切片,len=high-low,cap=max-low

    len(s)

    切片s的长度,总是<=cap(s)

    cap(s)

    切片s的容量,总是>=len(s)

      下面通过一个案例,演示一下:

      (1)s[:]

    func main() {
    	s := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    	slice := s[:]
    	fmt.Println("slice = ", slice)
    	fmt.Printf("len = %d,cap = %d", len(slice), cap(slice))
    }
    

      结果如下:

    slice =  [1 2 3 4 5 6 7 8 9 10]
    len = 10,cap = 10
    

      (2)s[low:]

    func main() {
    	s := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    	slice := s[3:]
    	fmt.Println("slice = ", slice)
    	fmt.Printf("len = %d,cap = %d", len(slice), cap(slice))
    }
    

      结果如下:

    slice =  [4 5 6 7 8 9 10]
    len = 7,cap = 7
    

      (3)s[:high]

    func main() {
    	s := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    	slice := s[:6]
    	fmt.Println("slice = ", slice)
    	fmt.Printf("len = %d,cap = %d", len(slice), cap(slice))
    }
    

      结果如下:

    slice =  [1 2 3 4 5 6]
    len = 6,cap = 10
    

      (4)s[low:high]

    func main() {
    	s := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    	slice := s[2:6]
    	fmt.Println("slice = ", slice)
    	fmt.Printf("len = %d,cap = %d", len(slice), cap(slice))
    }
    

      结果如下:

    slice =  [3 4 5 6]
    len = 4,cap = 8
    

      s[2:6] 表示从下标为2的元素(包含该元素)开始取,到下标为6的元素(不包含该元素)结束。所以切片slice的长度是4。切片slice的容量是多少呢?是8,根据s切片的容量是10, 减去s[2:6]中的2。

      现在定义一个切片s,然后对该切片s进行截取操作(范围自定义),得到新的切片slice, 并修改切片slice某个元素的值。代码如下:

    func main() {
    	s := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    	slice := s[2:6]
    	fmt.Println("slice = ",slice)
    }
    

      slice切片的结果是:[3 4 5 6] 因为是从下标为2的元素(包含)开始取,到下标为6的元素(不包含)结束,取出4个元素,也就是长度为4。

      现在将程序进行如下修改:

    func main() {
    	s := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    	slice := s[2:6]
    	slice[2] = 888 //对下标为2的元素进行修改
    	fmt.Println("slice = ", slice)
    }

      现在程序的输出结果是:

    slice =  [3 4 888 6]
    

      接下来输出切片s的值:

    func main() {
    	s := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    	slice := s[2:6]
    	slice[2] = 888 //对下标为2的元素进行修改
    	fmt.Println("slice = ", slice)
    	fmt.Println("s = ", s)
    }
    

      输出的结果如下:

    slice =  [3 4 888 6]
    s =  [1 2 3 4 888 6 7 8 9 10]
    

      我们发现切片s中的值也发生了变化,也就是修改切片slice的值会影响到原切片s的值。下面通过画图的形式来说明其原因。

     

      创建了切片s,然后对切片s进行截取slice := s[2:6],产生了新切片slice。slice指向切片s,范围从下标2到下标6(不包括6),对应的值是3,4,5,6,然后执行slice[2] = 888,对应切片slice[2]的值变成了888,当然切片s中的值也由5变成了888.

      我们再看一下两个切片中被修改的值对应的内存地址:

    func main() {
    	s := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    	slice := s[2:6]
    	slice[2] = 888 //对下标为2的元素进行修改
    	fmt.Println(&slice[2])
    	fmt.Println(&s[4])
    }

      结果如下:

    0xc00001e0c0
    0xc00001e0c0
    

      内存地址是一样的,所以我们可以认为slice := s[2:6],将s切片中的s[2],s[3],s[4],s[5]截取作为新切片slice,实际上是切片slice指向了原切片s(在这里并不是为切片slice新建一块区域)。所以修改slice,也会影响到s。

      下面继续修改上面的程序:

    func main() {
    	s := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    	slice := s[2:6]
    	slice[2] = 888 //对下标为2的元素进行修改
    	slice2 := slice[2:7]
    	fmt.Println("slice = ", slice)
    	fmt.Println("slice2 = ", slice2)
    	fmt.Println("s = ", s)
    }
    

      结果如下:

    slice =  [3 4 888 6]
    slice2 =  [888 6 7 8 9]
    s =  [1 2 3 4 888 6 7 8 9 10]
    

      下面也是通过画图的形式,来解释该程序的结果: 

      根据切片s创建新切片,执行代码:slice := s[2:6],表示从切片s下标为2开始取出4个数据,也就是3,4,5,6。作为新切片slice的值。当执行到代码slice[2] = 888时,将切片slice中下标值为2到值修改为888,也就是将原来的5修改为888,由于slice指向了原切片s,所以s中对应的值也由5变成888。所以这时候如果输出s的值就会得到[1 2 3 4 888 6 7 8 9 10]。当执行到slice2 := slice[2:7]时,根据切片slice创建一个新切片slice2,范围从切片slice的下标2开始(注意slice[2]的值已经变成了888),截取5个数(7-2=5),但slice中一共就4个数,怎么能截取出5个数呢,因为slice指向了原切片s,所以slice2也指向了原切片s(范围是上图灰色框中的数据),所以切片slice2的值为[888 6 7 8 9]。

      现在在原有的程序中又加了一行,如下所示:

    func main() {
    	s := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    	slice := s[2:6]
    	slice[2] = 888 //对下标为2的元素进行修改
    	slice2 := slice[2:7]
    	slice2[3] = 999
    	fmt.Println("slice = ", slice)
    	fmt.Println("slice2 = ", slice2)
    	fmt.Println("s = ", s)
    }
    

      最终,切片slice2与原来切片s的值分别是多少?

      结果如下所示:

    slice =  [3 4 888 6]
    slice2 =  [888 6 7 999 9]
    s =  [1 2 3 4 888 6 7 999 9 10]
    

      那么此时slice和slice2的长度和容量是多少呢?

    func main() {
    	s := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    	slice := s[2:6]
    	slice[2] = 888 //对下标为2的元素进行修改
    	slice2 := slice[2:7]
    	slice2[3] = 999
    	fmt.Printf("len(slice) = %d,cap(slice) = %d
    ", len(slice), cap(slice))
    	fmt.Printf("len(slice2) = %d,cap(slice2) = %d
    ", len(slice2), cap(slice2))
    }

      结果如下:

    len(slice) = 4,cap(slice) = 8
    len(slice2) = 5,cap(slice2) = 6
    

    2.7 copy函数使用

      针对切片操作常用的方法除了append( )方法以外,还有copy方法.

      基本语法:copy(目的切片,源切片)

      将第二个切片里面的元素,拷贝到第一个切片中。

      下面通过一个案例,看一下该方法的使用:

    func main() {
    	srcslice := []int{1, 2}
    	dstslice := []int{6, 6, 6, 6, 6}
    	copy(dstslice, srcslice)
    	fmt.Println("dst = ", dstslice)
    }
    

      上面案例中,将srcslice中的元素拷贝到dstslice切片中。结果如下:

    dst =  [1 2 6 6 6]
    

      通过以上结果可以分析出,直接将srcslice切片中两个元素拷贝到dstslice元素中相同的位置。而dstslice原有的元素备替换掉。

     

      下面将以上程序修改一下,如下所示:

    func main() {
    	srcslice := []int{1, 2}
    	dstslice := []int{6, 6, 6, 6, 6}
    	copy(srcslice, dstslice)
    	fmt.Println("src = ", srcslice)
    }
    

      以上程序的结果是:

    src =  [6 6]
    

       通过以上两个程序得出如下结论:在进行拷贝时,拷贝的长度为两个slice中长度较小的长度值,如果长度一样则全拷贝。

      思考以下程序输出的结果:

    func main() {
    	slice1 := []int{1, 2, 3, 4, 5}
    	slice2 := []int{5, 4, 3}
    	copy(slice2, slice1)
    	fmt.Println("slice2 = ", slice2)
    }
    

      结果是:

    slice2 =  [1 2 3]
    

     

      现在将程序进行如下修改:

    func main() {
    	slice1 := []int{1, 2, 3, 4, 5}
    	slice2 := []int{5, 4, 3}
    	copy(slice1, slice2)
    	fmt.Println("slice1 = ", slice1)
    }
    

      结果是:

    slice1 =  [5 4 3 4 5]
    

      思考下面代码的输出结果:

    func main() {
    	s1 := []int{1, 2, 3, 4, 5}
    	s2 := make([]int, 5)
    	copy(s2, s1)
    	s2[3] = 6
    	fmt.Println("s2 = ", s2)
    	fmt.Println("s1 = ", s1)
    }
    

      结果是:

    s2 =  [1 2 3 6 5]
    s1 =  [1 2 3 4 5]
    

      这说明修改复制后切片中的元素,原切片不会改变。

    2.8 切片作为函数参数

      切片也可以作为函数参数,那么与数组作为函数参数有什么区别呢?

      接下来通过一个案例,演示一下切片作为函数参数。

    func InitData(num []int) {
    	for i := 0; i < len(num); i++ {
    		num[i] = i
    	}
    }
    
    func main() {
    	s := make([]int, 10)
    	InitData(s)
    	fmt.Println(s)
    }
    

      输出结果:

    [0 1 2 3 4 5 6 7 8 9]
    

      通过以上案例,发现在主函数main( )中,定义了一个切片s,然后调用InitData( )函数,将切片s作为实参传递到该函数中,并在InitData( )函数中完成初始化,该函数并没有返回值,但是在主函数中直接打印切片s,发现能够输出对应的值。也就是在InitData( )函数中对形参切片num赋值,影响到了main( )函数中的切片s.

      但是,大家仔细想一下,如果我们这里传递参数不是切片,而是数组,那么能否完成该操作呢?

      那么我们将上面的程序,修改成以数组作为参数进行传递的形式:

    func InitData(num [10]int) {
    	for i := 0; i < len(num); i++ {
    		num[i] = i
    	}
    }
    
    func main() {
    	var s [10]int
    	InitData(s)
    	fmt.Println(s)
    }
    

      发现以数组的形式作为参数,并不能完成我们的要求,所以切片作为函数实参与数组作为函数实参,进行传递时,传递的方式是不一样的。

      在GO语言中,数组作为参数进行传递是值传递,而切片作为参数进行传递是引用传递。

      什么是值传递?什么是引用传递?

     

      值传递:方法调用时,实参数把它的值传递给对应的形式参数,方法执行中形式参数值的改变不影响实际参数的值

     

      引用传递:也称为传地址。函数调用时,实际参数的引用(地址,而不是参数的值)被传递给函数中相对应的形式参数(实参与形参指向了同一块存储区域),在函数执行中,对形式参数的操作实际上就是对实际参数的操作,方法执行中形式参数值的改变将会影响实际参数的值。

     

  • 相关阅读:
    Spring 基础学习
    Swagger basics (one)
    Handsontable Basics V7(one)
    JavaScript 对象
    CSS 基础总结
    Shell Programming(three)
    Shell Programming(two)
    Shell Programming(one)
    HTML标签总结
    jQuery 基础
  • 原文地址:https://www.cnblogs.com/dacaigouzi1993/p/11001545.html
Copyright © 2020-2023  润新知