Go Slice

作者: JunChow520 | 来源:发表于2020-12-31 01:45 被阅读0次

    切片(slice)是对数组的一个连续片段的引用,这个连续片段可以是整个数组,也可以是由起止索引标识的子集元素。需要注意的是切片终止索引标识的元素不包含在切片内。

    • 切片是一个引用类型,类似于C/C++中的数组类型或Python中的list列表类型。
    • 切片默认指向一段连续的内存区域,因此可以是数组也可以是切片本身。
    • 切片内部包括地址(指针)、大小(长度)、容量,常用于快速地操作一块数据集合。
    切片结构

    生成切片

    从连续内存区域生成切片

    目标切片对象 [开始位置:结束位置] 
    

    从数组中生成切片

    arr := [...]int{10, 20, 30}
    fmt.Println(arr)//[10 20 30]
    fmt.Println(arr[1:2])//[20]
    

    从数组或切片生成新切片具有的特性

    • 切出来元素的数量 = 结束位置 - 开始位置
    • 切出来的元素不包含结束位置对应的元素
    • 切片的最后一个元素可使用 slice[len(slice)]获取
    • 当缺省开始位置时默认表示从连续区域开头到结束位置
    • 当缺省结束位置时默认表示从开始位置到整个连续区域末尾
    • 当开始位置和结束位置都缺省时则于切片自身等效
    • 当开始位置和结束位置都等于0时等效于空切片,空切片可用于切片复位。

    根据索引位置获取切片元素值时,索引的取值范围从0到len(slice)-1,当索引越界时会报运行时错误。
    生成切片时,结束位置可使用len(slice),不会报错。

    从指定范围中生成切片

    var arr [30]int
    for i:=0; i<30; i++{
        arr[i] = i+1
    }
    
    fmt.Printf("arr[10:15] = %v\n", arr[10:15])
    // arr[10:15] = [11 12 13 14 15]
    
    fmt.Printf("arr[25:] = %v\n", arr[25:])
    // arr[25:] = [26 27 28 29 30]
    
    fmt.Printf("arr[:3] = %v\n", arr[:3])
    // arr[:3] = [1 2 3]
    
    • 切片类似C语言中的指针,指针可以做运算,代价是内存操作越界。
    • 切片在指针的基础上增加了大小,以约束切片对应的内存区域。
    • 切片使用中无法对切片内部的地址和大小进行手动调整,因此切片比指针更加安全。

    获取原有切片

    • 生成切片时当开始位置和结束位置都被忽略时表示和原有切片一样,这样生成的切片与原有切片在数据呢容上也是一致的。
    arr := [...]int{1, 2, 3}
    fmt.Printf("slice is %v\n", arr[:])//slice is [1 2 3]
    

    重置切片

    • 将切片的开始位置和结束位置重置为0时表示清空拥有的所有元素,切片将变空。
    arr := [...]int{1, 2, 3}
    fmt.Printf("reset slice %v\n", arr[0:0])//reset slice []
    

    声明切片

    每个类型都可以拥有自己的切片类型以表示多个相同类型元素的连续集合,因此切片类型是可以被声明的。

    切片类型声明格式:var 切片变量名 []元素类型

    例如:声明整型切片

    var intList []int
    fmt.Printf("intList = %v, len = %d\n", intList, len(intList))//intList = [], len = 0
    

    例如:声明空切片

    声明空切片时没有填充元素,因此切片是空的。但此时已经分配了内存,只是还没有元素。

    var strList = []string{}
    fmt.Printf("strList = %v, len = %d\n", strList, len(strList))//strList = [], len = 0
    

    例如:判断切片是否为空

    var intList []int
    fmt.Printf("intList = %v\n", intList)//intList = []
    fmt.Printf("len = %d\n", len(intList))//len = 0
    fmt.Printf("empty = %v\n", intList == nil)//empty = true
    

    空切片由于已经分配了内存,因此不等于nil

    var intList = []int{}
    fmt.Printf("intList = %v\n", intList)//intList = []
    fmt.Printf("len = %d\n", len(intList))//len = 0
    fmt.Printf("empty = %v\n", intList == nil)//empty = false
    

    构建切片

    • 可使用make()函数动态地构建一个切片
    make([]Type, size, cap)
    
    参数 描述
    Type 切片的元素类型
    size 为指定类型分配元素的个数
    cap 预分配元素的数量

    cap并不会影响size,只是能提前分配空间,以降低多次分配空间造成的性能损耗。

    x := make([]int, 2)
    y := make([]int, 2, 10)
    fmt.Printf("x = %v, y = %v\n", x, y)//x = [0 0], y = [0 0]
    fmt.Printf("x len = %v, y len = %v\n", len(x), len(y))//x len = 2, y len = 2
    

    构建切片与生成切片的方式之间的区别在于

    • 使用make()构建切片一定会发生内存分配操作
    • 使用给定开始和结束位置生成的切片只是将新切片结构指向已经分配好的内存空间,只是会设置开始和结束位置,并不会发生内存分配操作。

    追加元素

    • 使用Go语言内置函数append()可以为切片动态地追加元素
    var slice []int
    
    slice = append(slice, 1)
    fmt.Printf("slice = %v\n", slice)//slice = [1]
    
    slice = append(slice, 2, 3)
    fmt.Printf("slice = %v\n", slice)//slice = [1 2 3]
    
    • 可向切片中追加切片但需使用...解包
    var slice []int
    slice = append(slice, []int{1, 2, 3}...)
    fmt.Printf("slice = %v\n", slice)//slice = [1 2 3]
    
    • 切片容量不等于切片长度
    var slice []int
    for i:=0; i<3; i++{
        slice = append(slice, i)
        fmt.Printf("slice = %v, len = %d, cap = %d, pointer = %p\n", slice, len(slice), cap(slice), slice)
    }
    
    slice = [0], len = 1, cap = 1, pointer = 0xc0000100b0
    slice = [0 1], len = 2, cap = 2, pointer = 0xc0000100f0
    slice = [0 1 2], len = 3, cap = 4, pointer = 0xc00000c420
    

    每次向切片中追加元素都需要重新分配内存,相当于公司人数增加更换场地,公司不变切片名称不变,公署增加相当于切片元素增加,更换场地相当于重新分配内存,场地大小与公司人数不一定是相同的。

    头部追加

    var slice = []int{1, 2, 3}
    slice = append([]int{10, 20, 30}, slice...)
    fmt.Printf("slice = %v\n", slice)//slice = [10 20 30 1 2 3]
    
    • 切片头部追加元素会导致重新分配内存,原有元素会被复制一次,性能比尾部追加差。

    插入切片

    • append()函数会返回新的切片,因此可支持链式操作。
    • 将多个append()追加操作组合可实现向切片中插入元素
    var slice = []int{1, 2, 3}
    slice = append(slice[:1], append([]int{10, 20}, slice[1:]...)...)
    fmt.Printf("slice = %v\n", slice)//slice = [1 10 20 2 3]
    

    复制切片

    Go语言内置函数copy()可见一个数组切片复制到另一个数组切片中,若两个数组切片大小不一则按较小尺寸的来复制。

    copy(destSlice, srcSlice []T) int
    
    • 将来源切片srcSlice复制到目标切片descSlice
    • 目标切片必须已经分配过空间
    • 目标切片必须有足够看空间来承载来源切片元素的个数
    • 来源切片和目标切片类型必须保持一致
    • 复制函数返回值为实际发生复制的元素个数
    • 来源切片和目标切片可以共享同一个底层数组,即使发生重叠也没问题。
    srcSlice := []int{1, 2, 3}
    destSlice := []int{10, 20}
    count := copy(destSlice, srcSlice)
    fmt.Printf("srcSlice = %v\n", srcSlice)//srcSlice = [1 2 3]
    fmt.Printf("destSlice = %v\n", destSlice)//destSlice = [1 2]
    fmt.Printf("count = %d\n", count)//count = 2
    
    srcSlice := []int{1, 2, 3}
    destSlice := []int{10, 20, 30, 40}
    count := copy(destSlice, srcSlice)
    fmt.Printf("srcSlice = %v\n", srcSlice)//srcSlice = [1 2 3]
    fmt.Printf("destSlice = %v\n", destSlice)//destSlice = [1 2 3 40]
    fmt.Printf("count = %d\n", count)//count = 2
    

    切片的引用和复制操作后对切片的影响

    const cnt = 10
    
    src := make([]int, cnt)
    for i:=0; i<cnt; i++{
        src[i] = i
    }
    ref := src
    
    dest := make([]int, cnt)
    copy(dest, src)
    
    src[0] = 99
    fmt.Printf("src[0] = %v\n", src[0])//src[0] = 99
    fmt.Printf("ref[0] = %v\n", ref[0])//ref[0] = 99
    fmt.Printf("dest[0] = %v\n", dest[0])//dest[0] = 0
    fmt.Printf("dest[last] = %v\n", dest[cnt-1])//dest[last] = 9
    

    删除元素

    删除切片元素的本质是以被删除元素为分界点将前后两部分的内存重新连接起来,对于连续容器中的元素删除操作,由于需要将删除点前后的元素移动到新的位置,随着元素数量的增多,频繁操作的话对性能损耗较大。因此最好更换为其它类型的容器来操作,比如双链表。

    • 从开始位置删除切片元素

    删除切片开头的元素可以直接移动数据指针

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

    可将后面的元素向开头移动不移动数据指针,使用append()函数实现原地完成,原地完成是指在原有切片对应的内存空间内完成,因此不会导致内存结构的变化。

    slice := []int{1, 2, 3}
    slice = append(slice[:0], slice[1:]...)
    fmt.Printf("slice = %v\n", slice)
    

    可使用复制函数删除开头的元素

    slice := []int{1, 2, 3}
    slice = slice[:copy(slice, slice[1:])]
    fmt.Printf("slice = %v\n", slice)//slice = [2 3]
    
    • 从中间位置删除切片元素

    删除切片中的中间元素,需要对待删除的元素进行一次整体移动,可借助于appendcopy函数原地完成。

    slice := []int{1, 2, 3}
    slice = append(slice[:1], slice[2:]...)
    fmt.Printf("slice = %v\n", slice)//slice = [1 3]
    
    slice := []int{1, 2, 3}
    slice = slice[:1+copy(slice[1:], slice[2:])]
    fmt.Printf("slice = %v\n", slice)//slice = [1 3]
    
    • 从尾部删除切片元素
    slice := []string{"a", "b", "c"}
    index := 1
    slice = append(slice[:index], slice[index+1:]...)
    fmt.Printf("slice = %v\n", slice)//slice = [a c]
    

    迭代元素

    切片是多个相同类型元素的连续集合,由于切片是一个集合因此可迭代其中的元素。

    Go语言提供了range关键字,可配合for关键字来迭代切片中的元素。

    slice := []int{1, 2, 3}
    for index, value := range slice{
        fmt.Printf("index = %d, value = %d\n", index, value)
    }
    
    index = 0, value = 1
    index = 1, value = 2
    index = 2, value = 3
    

    当迭代切片时range关键字会返回两个值,第一个是当前迭代到的索引位置,第二个是该索引位置对应的元素值的副本。注意是副本,而非直接返回对该元素的引用。

    slice := []int{1, 2, 3}
    for index, value := range slice{
        fmt.Printf("index = %d, value = %d, valueAddr = %X, eleAddr = %X\n", index, value, &value, &slice[index])
    }
    
    index = 0, value = 1, valueAddr = C0000100B0, eleAddr = C00000C400
    index = 1, value = 2, valueAddr = C0000100B0, eleAddr = C00000C408
    index = 2, value = 3, valueAddr = C0000100B0, eleAddr = C00000C410
    

    由于迭代返回的变量是一个在迭代过程中根据切片依次赋值的新变量,因此元素值得地址都是相同的。

    range关键字默认会从切片头部开始迭代,若需要更多控制可使用传统的for循环。

    slice := []int{100, 200, 300}
    for i:=1; i<len(slice); i++{
        fmt.Printf("index = %d, value = %d\n", i, slice[i])
    }
    
    index = 1, value = 200
    index = 2, value = 300
    

    多维切片

    声明多维切片时,每个[]代表一个维度。

    例如:声明二维切片并赋值

    slice := [][]int{{10, 20, 30}, {100, 200}}
    fmt.Printf("slice = %v\n", slice)//slice = [[10 20 30] [100 200]]
    

    相关文章

      网友评论

          本文标题:Go Slice

          本文链接:https://www.haomeiwen.com/subject/vrxcoktx.html