Go语言基础、实战 -- 4. 数组、切片

一、数组

    1. 简介

        数组:是指相同元素的集合。

        元素:数组中包含的每个数据,被称为元素。

        长度:数组中元素的个数,被称为数组的长度。

        注意:数组长度也是数组类型的一部分,比如 [2]int 和 [3]int 两个数组,类型是不同的。

    2. 声明数组

        数组的类型为 [n]T ,n表示数组中元素的个数,T表示元素的类型,声明如下:

var arr1 [5]int    //如果没有初始化,数组中所有变量都会被默认初始化为0(Go特性)
arr1[0] = 1        //可以通过下标访问数组元素

arr2 := [5]int{1, 2, 3, 4, 5}           //数组也可以速记声明,还可以初始化
fmt.Printf("[arr2]: %v\n", arr2)        //打印数组中所有元素

         在声明数组时,还可以忽略数组长度,让编译器自动推导,如下:

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

    3. 动态数组?

            上面提到,数组的长度也是类型的一部分,[3]int和[2]int是两种不同的类型。因为Go语言的机制中,数组不能够动态

        地去改变长度。所以动态数组并不成立,但是切片可以弥补这个不足。

    4. 数组是值类型

            由于数组是值类型,用一个数组给另一个数组赋值时(长度和类型必须一致),会获取原数组的拷贝,新数组无论怎么

        修改,都不会影响原数组。

            所以在用数组进行赋值和传参的时候,本质上都是拷贝了一份到了新的数组,对新数组的操作不会影响到原数组。

func main() {
    arr_int1 := [...]int{1, 2, 3, 4, 5}
    arr_int2 := arr_int1
    arr_int2[1] = 22         //改变新数组中第二个元素的值
    fmt.Printf("[arr_int1]: %v\n", arr_int1)    //对新数组的操作不会对原数组有影响
    fmt.Printf("[arr_int2]: %v\n", arr_int2)    //新数组的值已经改变

    ChmodArray(arr_int1)
    fmt.Printf("[arr_int1]: %v\n", arr_int1)    //传参时也一样,对新数组的操作不会对原数组有影响
}

func ChmodArray(arr_int [5]int) {
    arr_int[1] = 22
}

    5. 数组的长度

        Go语言为我们提供了一个内置函数 len(),可以获取数组、切片、map等容器的长度。

fmt.Printf("[arr_int1 Length: %d]\n", len(arr_int1))

    6. range遍历数组

        前面的章节提到了range遍历,十分简单好用。

        它会从容器的第一个元素遍历到最后一个元素,它最多会返回两个值。可以只接收前面一个值,也可以都接收。

        当它遍历数组时,它返回的值分别是下标,还有下标所在元素的值。如下:

for i := range arr_int1 {        //可以只接收下标的值
    fmt.Printf("%d ", i)
}
for i, v := range arr_int1 {     //也可以接收下标和下标所在元素的值
    fmt.Printf("(arr_int[%d] = %d)\n", i, v)
}

for _, v := range arr_int1 {     //当不需要下标,只需要值时,可以用下划线 _
    fmt.Printf("%d ", v)
}

        注意: 

            v:这里的变量v,也是一个值传递,修改v的值并不会对当前遍历的数组有影响(切片也一样)

            i:而通过下标i来修改值时,就会对原数组产生影响了(切片也一样)

    7. 二维数组

        1) 初始化

            用以下的方式来初始化,每一行后面都必须写逗号,后面的struct也是一样。如下:

mutil_arr := [3][2]string {
    {"2018MVP", "James Harden"},        //这里必须写逗号','
    {"2017MVP", "Russell Westbrook"},
    {"2016MVP", "Stephen Curry"},
}

    2) range for

        用 range for 来遍历二维数组,v是什么类型呢?并不难想,它其实是一维数组,如下:

for i, v := range mutil_arr {        // 这里v的类型是一维数组
    fmt.Printf("[mutil_arr %d]: %v\n", i, v)
}

二、切片

          切片是建立在数组之上的更方便、更灵活、更强大的数据结构。切片并不存储任何元素,只是对现有数组的引用

          注意:切片只是对数组的引用!

    1. 声明

            切片可以跟数组一样声明,唯一的区别是不需要写长度了;除此之外,由于它本质是对数组的引用,还可以用数

        组来初始化。

        1) 只声明不初始化

var slice_int []int
if slice_int == nil {
	fmt.Println("[slice_int]: [slice_int is empty]")
}

            像这样声明的切片是空的,没有任何元素,因为没有对任何数组进行引用,也不能用下标来访问。

        2) 用make初始化

            make也是Go语言的内置函数,可以用来创建数组、切片、map和channel(通道,后面会讲)。

            用make创建切片时,语法为make([]T, len, cap),len为长度,cap为容量,如下:

slice_byte := make([]byte, 5, 5)
fmt.Printf("[slice_byte]: %v\n", slice_byte)    //输出结果:[0 0 0 0 0]

                与上面的声明方式不同,make会去申请内存,所以这里相当于在内存中申请了一个长度为5,容量为5的数组,

            然后用切片slice_byte对其进行引用,同时,还对每个元素进行了初始化,给它们赋值为对应类型的零值。所以这样

            声明的切片,就能直接用下标访问了。

            长度:指切片中元素的个数

            容量:指切片所引用的数组最多能容纳的元素个数 (后面内存原理部分还会有详细的讲解)

            我们把上面的初始化改一下,第二个参数len的值改为4,看看会发生什么变化:

slice_byte2 := make([]byte, 4, 5)
fmt.Printf("[slice_byte2]: %v\n", slice_byte2)    //输出结果为:[0 0 0 0]

            看到了吗,由于长度的值我们给的是4,现在切片中只有4个元素了。

        3) 声明并初始化

slice_int2 := []int{1, 2, 3, 4, 5}
fmt.Printf("[slice_int2]: %v\n", slice_int2)

            像这样声明的切片,实际上是在内存中创建了一个数组,然后让slice_int2去引用它。

        4) 声明并用数组来初始化

arr_string := [5]string{"Alston", "T_MAC", "Battier", "Scola", "Yao"}
slice_string := arr_string[0:3]
slice_string2 := arr_string[0:5]           //引用整个数组
//slice_string2 := arr_string[:]           //引用整个数组
fmt.Printf("[slice_string]: %v\n", slice_string)
fmt.Printf("[slice_string2]: %v\n", slice_string2)

            还可以像这样用数组来初始化,中括号内的语法 [Start : End],表示想要引用原数组下标为Start的元素到End-1的元素。

            同时还可以不写start和end,像这样:[:],也是对整个数组引用

            注意:

                a. 写End表示的是到下标为End-1位置的元素,而不是End,所以想要引用上面整个数组,应该是0到len(arr_string)

                b. 在Go语言中,如果下标越界,能在编译时确定的会发生变异错误。运行时发生下标越界,会panic,程序崩溃

    2. 基本操作

            对切片操作会对它引用的数组有影响。

        1) 下标操作

            切片可以通过下标来操作,唯一需要注意的是下标的合法性,如果下标超过了切片中的元素,就会出错:

fmt.Printf("slice_string[2]: %s\n", slice_string[2])
// fmt.Printf("slice_string[3]: %s\n", slice_string[3])		//错误,下标越界

        2) append函数

            前面已经提到了,切片是动态地,所以Go提供了内置函数append来给切片动态地添加元素.

            函数原型为:append(s []T, x... T)

            第二个参数为变参函数,我们可以传入任意个数的值,也可以传入切片(前面的章节提到过,切片可以给变参函数传值)

var slice_string3 []string
slice_string4 := []string{"Python", "Java"}
slice_string3 = append(slice_string, "C++", "Go", "Rust")	//添加任意数量的元素
slice_string3 = append(slice_string, slice_string4...)		//添加切片,注意切片后面一定要写...
fmt.Printf("[slice_string3]: %v\n", slice_string3)

            append函数的功能就是,将第二个参数一个或多个元素添加到第一个参数(切片) 后面,然后再返回这个新的切片。

       3) 遍历切片 

            与数组没啥区别,不写了

       4) 作为函数参数

            数组为值类型,传参时实际上发生了拷贝,所以不管函数内如何操作,不会对原数组产生影响。

            而切片不同,它的本质是引用,所以在函数内的操作会对切片产生影响。如下:

func main() {
    num_slice := []int{1, 2, 3, 4, 5}
	ChmodSlice(num_slice)
	fmt.Printf("[num_slice]: %v\n", num_slice)		//输出结果:[num_slice]: [1 22 3 4 5]
}

func ChmodSlice(num_slice []int) {
	num_slice[1] = 22
}

    3. 内存原理 

            首先,切片是对数组的引用,所以只要数组还在被切片引用,它就不会被垃圾回收。         

            之前提到数组不是动态的,长度是不能改变的,而切片是动态地,那么切片作为数组的引用,当它里面元素个数已经达

        到数组长度之后,再去append新的元素,会出错吗?答案是不会。让我们一起来看看这个过程的原理。

            我们现在已经知道,切片是对数组的引用,对切片的一切操作,实际上是对数组进行操作,那么当切片中的元素个数还

        没有达到容量时,我们往切片内添加元素,实际上还是在对数组进行操作。而当切片中的元素已经达到容量,再去添加元

        素,这时原数组已经容纳不下了,而长度又不能改变,所以Go会在底层申请一个新的数组,然后将之前那个数组的元素全

        部拷贝过来,再用切片来引用这个新数组,这个切片的容量是之前那个的2倍。

            下面通过两个例子代码来具体看一下。

        1) 当切片长度已达到容量,append

//当切片长度已达到容量,再添加元素
car_array := [5]string{"BMW", "Benz", "Audi", "Toyota", "Honda"}
car_slices := car_array[:]
car_slices[4] = "KIA"
fmt.Printf("[car_array]: %v\n", car_array)		//切片car_slices引用原数组car_array,对切片的操作也会影响到原数组
fmt.Printf("[car_slices (cap : %d)]: %v\n", cap(car_slices), car_slices)	//输出结果:[car_slices (cap : 5)]: [BMW Benz Audi Toyota Honda]

car_slices = append(car_slices, "Cadillac")
fmt.Printf("[car_slices (cap : %d)]: %v\n", cap(car_slices), car_slices)	//输出结果:[car_slices (cap : 10)]: [BMW Benz Audi Toyota Honda Cadillac]

car_slices[4] = "Volvo"
fmt.Printf("[car_array]: %v\n", car_array)		//切片引用了新的数组,对切片的操作不会影响到原数组car_array

            可以看到,正如上面说的一样,当切片长度增加到原数组已经容纳不下,就会申请一个容量为之前2倍的新数组。

            现在切片就和之前的数组没有引用关系了,对切片操作不会影响原数组。

        2) 当切片长度未到容量时,append

            这种情况下,对切片进行append实际上是对原数组下一个位置的元素进行更改:

//当切片长度未到容量时,添加元素
phone_array := [...]string{"iPhone", "samsung", "NOKIA", "HUAWEI", "XIAOMI", "Vivo", "Oppo"}
var phone_slices []string = phone_array[1:5]
fmt.Printf("[phone_array]: %v\n", phone_array)  	//输出结果为:[iPhone samsung NOKIA HUAWEI XIAOMI Vivo Oppo]

phone_slices = append(phone_slices, "MEIZU")
fmt.Printf("[phone_array]: %v\n", phone_array)  	//输出结果为:[iPhone samsung NOKIA HUAWEI XIAOMI MEIZU Oppo]

            注意到了吗,切片引用了原数组第2个到第5个元素,当我们对切片进行append的时候,原数组第6个元素的值改变了。

            

        实际使用中其实我们不必过多地担心内存方面的问题, 因为一般都是直接声明切片,很少用先创建数组,再声明切片引用

    它。所以我们不必过多的担心对原数组的影响,以及内存的问题,Go会为我们搞定一切。

这一章的内容就全部说完了,有点长,但是不难理解,多用用就会很熟练了。

发布了16 篇原创文章 · 获赞 15 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/zhounixing/article/details/103299563