• go切片的nil 切片、空切片与零切片(重要)


    nil 切片、空切片与零切片是切片的三种状态,nil 切片是指在声明时未做初始化的切片,不用分配内存空间,一般使用 var 创建。使用 make 创建的空切片需要分配内存空间,nil 切片与空切片的长度、容量都为 0 ,如果我们要创建长度容量为 0 的切片,官方推荐 nil 切片。零切片指初始值为类型零值的切片。

    // 创建 nil 切片
    var slice []int
    fmt.Println(slice,*(*reflect.SliceHeader)(unsafe.Pointer(&slice))) // 输出:[] {0 0 0}

    // 创建空切片
    slice2 := make([]int,0)
    slice3 := []int{}
    fmt.Println(slice2,*(*reflect.SliceHeader)(unsafe.Pointer(&slice2))) // 输出:[] {18504816 0 0}
    fmt.Println(slice3,*(*reflect.SliceHeader)(unsafe.Pointer(&slice3))) // 输出:[] {18504816 0 0}

    // 创建零切片
    slice4 := make([]int,2,5)
    fmt.Println(slice4,*(*reflect.SliceHeader)(unsafe.Pointer(&slice4))) // 输出:[0 0] {824634474496 2 5}
    • 基本操作
    slice := []string{"Go","语","言","编","程"}

    // 1.访问元素
    fmt.Println(slice[0])         // 输出:Go

    // 2.修改元素
    slice[0] = "PHP"
    fmt.Println(slice) // 输出:[PHP 语 言 编 程]
    • 特殊操作
    1. 追加新元素(扩容操作)

    append() 函数 是 Go 语言里专为切片类型提供的操作函数。切片长度不足时,使用 append() 函数,可在切片头部或尾部追加新元素,但是,由于在头部追加元素会导致内存重新分配,所有元素将复制一次,因此大多数情况下推荐在尾部追加。追加的新元素可以是一个或多个,甚至是一个切片。append() 函数能实现切片自动增加长度并按需扩容。

    slice := []string{"Go","语","言","编","程"}
    // 1.在切片尾部追加一个元素
    slice = append(slice, "!")
    fmt.Println(slice,len(slice), cap(slice))   // 输出:[Go 语 言 编 程 !] 6 10

    // 2.在切片尾部追加多个元素
    slice = append(slice,"!","!")
    fmt.Println(slice,len(slice), cap(slice)) // 输出:[Go 语 言 编 程 ! ! !] 8 10

    // 3.在切片尾部追加切片
    slice = append(slice,[]string{"!","!"}...)
    fmt.Println(slice,len(slice), cap(slice)) // 输出:[Go 语 言 编 程 ! ! ! ! !] 10 10

    // 4.在切片头部追加切片
    slice = append([]string{"最","爱"},slice...)
    fmt.Println(slice,len(slice), cap(slice)) // 输出:[最 爱 Go 语 言 编 程 ! ! ! ! !] 12 12

    一个数组包含类型与长度两部分,切片的底层是一个数组,切片容量等于数组长度,在这个前提下,我们创建切片并修改它的长度时,如果长度小于容量(数组长度),那么函数将直接在原底层数组上增加新的元素,如果切片长度超出容量(数组长度),append() 函数就会创建一个新的底层数组,再将源数组的值复制过来,我们可以通过对比 SliceHeader.Data 字段观察底层数组发生的变化。

    // 发生扩容前
    slice := []string{"Go","语","言","编","程"}
    fmt.Println(*(*reflect.SliceHeader)(unsafe.Pointer(&slice)))    // 输出:{824634458112 5 5}

    // 发生扩容后
    slice = append(slice, "!") 
    fmt.Println(*(*reflect.SliceHeader)(unsafe.Pointer(&slice)))    // 输出:{824634466464 6 10}

    append() 扩容逻辑大概是这样,当 size 小于 1024 字节时,按乘以 2 的长度创建新的底层数组,超过 1024 字节时,按 1/4 增加。

    1. 删除切片元素(缩容操作)

    前面说到切片是动态的数组,灵活又方便,既能使用 append() 函数对它进行扩容,也能使用 [:] 运算符在源切片上创建新的切片,实现元素的删除。:左边是开始位,右边是结束位,它们表示元素开始与结束的选取范围。

    // 创建一个空切片
    slice := make([]string,0)
    fmt.Println(slice,*(*reflect.SliceHeader)(unsafe.Pointer(&slice)))  // 输出:[] {18541680 0 0}

    // 使用 append 函数在切片尾部追加一个切片,触发扩容,内存重新分配
    slice = append(slice,[]string{"最","爱","Go","语","言","编","程","!","!"}...)
    fmt.Println(slice,*(*reflect.SliceHeader)(unsafe.Pointer(&slice)))  // 输出:[最 爱 Go 语 言 编 程 ! !] {824634204160 8 9}

    // 从切片尾部删除元素,赋值给新切片,新切片与源切片共享同一个底层数组
    newSlice := slice[0:8]
    fmt.Println(newSlice,*(*reflect.SliceHeader)(unsafe.Pointer(&newSlice))) // 输出:[最 爱 Go 语 言 编 程 !] {824634204160 8 9}

    // 从切片尾部删除元素,创建新切片和新的底层数组
    var newSlice2 []string
    newSlice2 = append(newSlice2,slice[0:8]...)
    fmt.Println(newSlice2,*(*reflect.SliceHeader)(unsafe.Pointer(&newSlice2))) // 输出:[最 爱 Go 语 言 编 程 !] {824634212352 8 8}

    // 从切片头部和尾部删除元素并返回新切片,内存重新分配
    slice = slice[2:7]
    fmt.Println(slice,*(*reflect.SliceHeader)(unsafe.Pointer(&slice)))  // 输出:[Go 语 言 编 程] {824634204192 5 7}

    通过上面的代码,我们得出一个结论:从切片尾部删除元素不会触发缩容,仅长度发生变化,从切片头部删除元素则会触发缩容,长度及容量都发生变化。所以删除的元素不再使用后,一般建议申请新的内存空间,创建新的切片来接收要保留的元素,这样可以避免原底层数组内存无效占用,从源切片头部删除不存在此问题。

    1. 拷贝切片

    Go 语言提供了内置函数 copy() 用来拷贝切片。在前面的代码里用 append() 函数演示了如何从切片头部或尾部删除元素,那如果想从中间删除应该怎么做?还有,前面说到当切片容量不足以添加新元素时,append() 函数会依据规则创建新的大容量底层数组,在此基础创建新切片,再将源切片的内容拷贝到新切片中,那它是如何操作的呢?

    // 源切片
    slice := []string{"Go","语","言","言","言","编","程","!"}
    // 创建新切片
    newSlice := make([]string,6)
    // 选择范围拷贝到新切片
    at := copy(newSlice,slice[0:3])
    copy(newSlice[at:],slice[5:8])
    fmt.Println(newSlice)  // 输出:[Go 语 言 编 程 !]

    copy() 函数的操作使用 append() 函数也能实现,只不过 append() 函数每次复制数据都需要创建临时切片,相比性能上 copy() 函数更胜一筹。

    说到拷贝,它可分为深拷贝与浅拷贝,我们一般默认值类型的数据是深拷贝,引用类型的数据是浅拷贝,那深拷贝与浅拷贝有什么区别呢?深拷贝是指使用数据时,基于原始数据创建一个副本,有独立的底层数据空间,之后的操作都在副本上进行,对原始数据没有任何影响。反之,操作对原始数据有影响的叫做浅拷贝,比如拷贝原始数组地址。我们可以回想一下,在前面的切片操作中,哪些是深拷贝,哪些是浅拷贝。

    1. 判断 2 个字符串切片是否相等

    Go 语言标准库中有专门的方法判断两个字节切片是否相等,却没有针对字符串切片的,当我们要判断两个字符串切片是否相等时,可以使用 reflect 包提供的 DeepEqual() 方法,或者自定义。但不管哪种,在使用前都要先定义什么是相等,一个切片包含类型、长度、容量和元素值,按照以往的经验,当类型、元素值、长度一致时,我们便认为这两个切片是相等的。只是,使用自定义方法需要先确定类型再比较,而使用 reflect.DeepEqual() 可以比较两个不确定类型的值,但是需要付出很大的性能代价,所以通常我们还是使用前者。

    // 创建两个长度相同、容量不同的切片
    slice1 := make([]string,2,5)
    slice1[0] = "Go"
    slice1[1] = "语"
    slice2 := make([]string,2,6)
    slice2[0] = "Go"
    slice2[1] = "语"
    // 方法1:自定义方法
    status := true
    if len(slice1) != len(slice2){
       status = false
    }else{
       for k,_ := range slice1 {
        if slice1[k] != slice2[k] {
         status = false
        }
       }
    }
    fmt.Println(status)     // 输出:true
    // 方法2:使用 reflect.DeepEqual
    fmt.Println(reflect.DeepEqual(slice1,slice2))  // 输出:true

    总结

    数组跟切片是 Go 语言中很基础的内容,它们的操作也不仅限于前面介绍的那些。切片在某种程度上是来源于数组的,所以他们可以支持相同的操作方法,但切片的形式更为动态,操作方法和使用场景也更加丰富。

    这次总结 Go 语言数组跟切片的内容花费了很长时间,尽管如此,仍然有一些”未解之谜“,对于已了解的内容也不敢说都理解恰当,我将带着这些问题,继续 Go 语言学习之路

     
  • 相关阅读:
    Java类、实例初始化的顺序
    Java中堆栈的区别
    int与Integer的区别(基本类型与复杂类型的对比)转
    JS中函数执行顺序的问题?
    springMVC --@RequestParam注解(后台控制器获取参数)
    如何安装request库
    流程图
    认识broken pipe
    postman动态使用url
    自定义一个List排序规则,然后对另一个List按照该规则排序
  • 原文地址:https://www.cnblogs.com/cheyunhua/p/16103746.html
Copyright © 2020-2023  润新知