zoukankan      html  css  js  c++  java
  • Golang的高级数据类型-切片(slice)实战篇

              Golang的高级数据类型-切片(slice)实战篇

                                 作者:尹正杰

    版权声明:原创作品,谢绝转载!否则将追究法律责任。

     

     

       切片(slice)是Go中一种比较特殊的数据结构,这种数据结构更便于使用和管理数据集合,切片是围绕动态数组的概念构建的,可以按需自动增长。

      

     

    一.切片的基本使用 

    1>.切片的定义和初始化赋值

    package main
    
    import (
        "fmt"
    )
    
    func main() {
    
        /*
            什么是切片(slice):
                切片是Go中一种比较特殊的数据结构,这种数据结构更便于使用和管理数据集合。
                切片是围绕动态数组的概念构建的,可以按需自动增长。
    
            切片的定义格式:
                语法一(定义空(nil)切片):
                    var 切片名称 []数组类型
    
                语法二(通过make创建切片):
                    var 切片名称 []数组类型 = make([]数据类型,长度,容量)
    
                语法三(自动推导类型初始化):
                    切片名称 := []数组类型{元素1,元素2,...,元素N}
                    切片名称 := make([]数据类型,长度,容量)
    
            切片的长度(len)与容量(cap):
                len(切片名称):
                    计算切片有效数据个数,长度是已经初始化的空间,切片初始空间若为被赋值则使用对应数据类型的默认值。
                cap(切片名称):
                    计算切片容量,容量是一家开辟的空间,包括一家初始化的空间和空闲的空间。
    
            常见数组类型的默认值:
                整型默认初始化值为0;
                浮点型默认初始化值为0;
                字符串类型默认初始化值为空串("");
                布尔类型默认初始化值为false;
        */
    
        //定义一个空(nil)切片,空切片不能添加数据
        var slice1 []int
        fmt.Printf("slice1的数据为:%d,长度为:%d,容量为:%d
    ", slice1, len(slice1), cap(slice1))
    
        //通过make创建切片
        var slice2 []int = make([]int, 3, 5)
        fmt.Printf("slice2的数据为:%d,长度为:%d,容量为:%d
    ", slice2, len(slice2), cap(slice2))
    
        //基于切片索引进行赋值时,其索引大小不能等于或超过切片的长度,否则就会报错"index out of range"
        slice2[1] = 666
        fmt.Printf("slice2的数据为:%d,长度为:%d,容量为:%d
    ", slice2, len(slice2), cap(slice2))
    
        //自动推导类型创建切片
        slice3 := []int{1, 2, 3, 4, 5}
        slice4 := make([]int, 2, 5)    //make函数可以理解为给slice4这个切片申请空间
        fmt.Printf("slice3的数据为:%d,长度为:%d,容量为:%d
    ", slice3, len(slice3), cap(slice3))
        fmt.Printf("slice4的数据为:%d,长度为:%d,容量为:%d
    ", slice4, len(slice4), cap(slice4))
    
    }

    2>.切片作为函数参数(本质上是引用地址传递)

    package main
    
    import (
        "fmt"
    )
    
    func Demo(s1 []int32) {
        //修改切片元素的值
        s1[0] = 'Y'
    
        fmt.Printf("s1的数据为:[%s],内存地址为:[%p]
    ", string(s1), s1)
    }
    
    func main() {
    
        char := []rune{'', '', '', '', '', '', ''}
        fmt.Printf("char的数据为:[%s],内存地址为:[%p]
    ", string(char), char)
    
        /*
           在Go语言中,数组作为参数进行传递时值传递,而切片作为参数进行传递时引用传递。
               值传递:
                   方法调用时,实参数把他的值传递给对应的形式参数,方法执行中形式参数值的改变不会影响实际参数的值。
               引用传递(也称为传地址):
                   函数调用时,实际参数的引用(地址,而不是参数的值)被传递给函数中相对应的形式参数(实参与形参指向了同一块存储区域);
                   在函数执行时,对形式参数的操作实际上就是对实际参数的操作,方法执行中形式参数值的改变会影响实际参数的值。
    
           温馨提示:
               建议在开发中使用切片代替数组。
        */
        Demo(char)
    
        fmt.Printf("char的数据为:[%s],内存地址为:[%p]
    ", string(char), char) //主线程调用函数切片元素的值被修改
    }

    3>.切片中的指针问题刨析

    package main
    
    import (
        "fmt"
    )
    
    func main() {
    
        //定义一个切片
        s1 := []rune{'', '', ''}
    
        /*
            C++转过来学Go还容易理解指针,但是从Python和Java转过来学Go时接触Go语言的小伙伴是不是很抵触指针呢?
    
            我很好奇格式化输出有一个"%p",但是我传递切片"s1"和"&s1"是不一样的,这是为什么呢?
        */
        fmt.Printf("char的数据为:[%s],内存地址为:[%p]
    ", string(s1), s1)
    
        fmt.Printf("char的数据为:[%s],内存地址为:[%p]
    ", string(s1), &s1)
    
        /*
            指针问题刨析:
                切片名s1是指向内存空间,所以s1其实存的是切片在内存中的存储地址。
                而"&s1"其实打印的是存储地址空间的地址(即s1变量本身的地址),换句话说,s1本身也是一个变量,它也有自己的内存地址。
        */
    }

      问题:
        我们在使用"fmt"包中"Printf"方法中的"%p"进行格式化时,为什么数组都需要加取值符("&"),而切片却不用呢?
      问题刨析:
        首先,不是
    "%p"就一定能够打印地址,变量本身存储的是地址,才能打印地址。接下来我们分析一下数组和切片的关系:       数组是可以直接存储数据的,如果想要打印数组的地址,首先得使用取值符("&")来取地址。       切片并不是直接存储数据的,切片需要先使用make申请空间,然后得到一个空间的引用地址才能基于该引用地址存储数据。也就是说切片本身就是一个引用地址。

    二.切片扩容

    package main
    
    import (
        "fmt"
    )
    
    func main() {
    
        /*
           切片的扩容:
               切片的长度时不固定的,可以向已经定义的切片中追加数据。
               通过append函数可以在原切片的尾部添加元素
    
           切片的结构如下所示,其中unsafe.Pointer对应的类型为:
               type slice struct {
                   array unsafe.Pointer
                   len   int
                   cap   int
               }
               接下来我们来刨析一下slice结构体:
                   array:
                       存储的是数组指针,它指向了数组的内存地址。
                       其类型"unsafe.Pointer"对应“type Pointer *ArbitraryType”,而"ArbitraryType"对应的是"int",而int在64为操作系统上来讲就是int64,对应8字节。
                   len:
                       int类型对应的是8字节
                   cap:
                       int类型对应的是8字节
        */
        s1 := []int{100, 200, 300} //使用自动推导类型定义切片
        fmt.Printf("s1的长度为:%d,容量为:%d,内存地址为:%p
    ", len(s1), cap(s1), s1)
    
        /*
           向slice1切片中添加(扩容)了5个元素,尽管slice1之前的容量是3,但依旧是可以往里面添加数据的哟~而且并不会改变slice1的内存地址。
        */
        s1 = append(s1, 100)
        fmt.Printf("s1的长度为:%d,容量为:%d,内存地址为:%p
    ", len(s1), cap(s1), s1)
    
        s1 = append(s1, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11)
        fmt.Printf("s1的长度为:%d,容量为:%d,内存地址为:%p
    ", len(s1), cap(s1), s1)
    
        /*
            如果切片扩容超过切片的容量,那么Go语言会该切片重新申请一个内存空间,把原来切片中的数据拷贝到新的地址空间,因此在生产环境中建议大家不要将切片的容量设置过小。
    
            切片的扩容规则(参考源码文件"runtime/slice.go"的"func growslice(et *_type, old slice, cap int) slice"函数):
                如果长度和容量相等分为下面三种情况:
                  1>.长度和容量小于等于896时:
                    再次为切片添加数据时,切片会自动扩容,扩容大小为上一次的2倍。
                  2>.长度和容量大于896小于1024时:
                    再次为切片添加数据时,切片会自动扩容,扩容大小固定为2048。
                  3>.长度和容量大于等于1024时:
                    再次为切片添加数据时,切片会自动扩容扩容为上一次的四分之一(1/4)。
    
        */
        s2 := make([]int, 896, 896)
        fmt.Printf("s2的长度为:%d,容量为:%d,内存地址为:%p
    ", len(s2), cap(s2), s2)
        s2 = append(s2, 1) //
        fmt.Printf("s2的长度为:%d,容量为:%d,内存地址为:%p
    ", len(s2), cap(s2), s2)
    
        s3 := make([]int, 897, 897)
        fmt.Printf("s3的长度为:%d,容量为:%d,内存地址为:%p
    ", len(s3), cap(s3), s3)
        s3 = append(s3, 2) //
        fmt.Printf("s3的长度为:%d,容量为:%d,内存地址为:%p
    ", len(s3), cap(s3), s3)
    
        s4 := make([]int, 1023, 1023)
        fmt.Printf("s4的长度为:%d,容量为:%d,内存地址为:%p
    ", len(s4), cap(s4), s4)
        s4 = append(s4, 2) //
        fmt.Printf("s4的长度为:%d,容量为:%d,内存地址为:%p
    ", len(s4), cap(s4), s4)
    
        s5 := make([]int, 1024, 1024)
        fmt.Printf("s5的长度为:%d,容量为:%d,内存地址为:%p
    ", len(s5), cap(s5), s5)
        s5 = append(s5, 2) //
        fmt.Printf("s5的长度为:%d,容量为:%d,内存地址为:%p
    ", len(s5), cap(s5), s5)
    
        s6 := make([]int, 1025, 1025)
        fmt.Printf("s6的长度为:%d,容量为:%d,内存地址为:%p
    ", len(s6), cap(s6), s6)
        s6 = append(s6, 2)
        fmt.Printf("s6的长度为:%d,容量为:%d,内存地址为:%p
    ", len(s6), cap(s6), s6)
    }

    三.切片的截取

    package main
    
    import "fmt"
    
    func main() {
        /*
            切片的截取:
                所谓截取就是从切片中获取指定的数据。
    
            切片常见的操作如下所示:
                "slice[n]":
                    切片slice中索引位置的为n的选项。
                "slice[:]":
                    从切片slice的索引位置0到len(slice)-1处获得的切片
                "slice[low:]":
                    从切片slice的索引位置low到len(slice)-1处获得的切片
                "slice[:high]":
                    从切片selice的索引位置0到high处所获得的切片,len=high-low
                "slice[low:high:max]":
                    从切片slice的索引位置low到high处所获得的切片,len=high-low,cap=max-low
                "len(s)":
                    表示切片s的长度,切片的长度总是小于等于(>=)切片容量的(cap(s))
                "cap(s)":
                    表示切片s的容量,切片的容量总是大于等于(<=)切片的长度(len(s))
        */
    
        slice1 := []rune{'', '', '', '', '', '', ''}
        fmt.Printf("slice1的数据为:[%s],长度为:[%d],容量为:[%d]
    ", string(slice1), len(slice1), cap(slice1))
    
        s1 := slice1[2] //获取切片索引为2的元素
        fmt.Printf("s1的数据为:[%s]
    ", string(s1))
    
        s2 := slice1[:] //默认截取的长度和容量相等
        fmt.Printf("s2的数据为:[%s],长度为:[%d],容量为:[%d]
    ", string(s2), len(s2), cap(s2))
    
        s3 := slice1[3:] //设置截取的起始索引,左闭右开
        fmt.Printf("s3的数据为:[%s],长度为:[%d],容量为:[%d]
    ", string(s3), len(s3), cap(s3))
    
        s4 := slice1[:4] //设置截取的结束索引,左闭右开
        fmt.Printf("s4的数据为:[%s],长度为:[%d],容量为:[%d]
    ", string(s4), len(s4), cap(s4))
    
        s5 := slice1[1:3] //设置截取的起始索引和结束索引,左闭右开
        fmt.Printf("s5的数据为:[%s],长度为:[%d],容量为:[%d]
    ", string(s5), len(s5), cap(s5))
    
        s6 := slice1[1:3:6] //设置截取的起始索引和结束索引并指定容量
        fmt.Printf("s6的数据为:[%s],长度为:[%d],容量为:[%d]
    ", string(s6), len(s6), cap(s6))
    }

    四.切片的浅拷贝(虽然Go语言支持二维数组,但不建议大家使用,推荐使用字典)

    package main
    
    import (
        "fmt"
    )
    
    func main() {
    
        /*
           Go语言的内置函数copy()可以将一个切片复制到另一个切片。
           如果加入的两个数组切片不一样,就会按其中较小的那个数组切片的元素个数进行复制。
        */
        src := []int32{'', '', '', '', '', '', ''}
        fmt.Printf("src的数据为:[%s],长度为:[%d],容量为:[%d],内存地址为:[%p]
    ", string(src), len(src), cap(src), src)
    
        //创建一个长度为5的元素大小的切片
        dest := make([]rune, 5)
    
        //切片拷贝,会根据较小的切片进行拷贝,由于dest的容量为5,而src的容量为7,将src的元素拷贝到dest时会按照容量较小的(即dest的容量)来进行拷贝
        copy(dest, src)
        fmt.Printf("dest的数据为:[%s],长度为:[%d],容量为:[%d],内存地址为:[%p]
    ", string(dest), len(dest), cap(dest), dest)
    
    }

    五.删除切片(生产环境中不推荐大家删除切片的元素,如果非要删除建立考虑从尾部删除,具体原因看下面案例你就知道啦~)

    package main
    
    import (
        "fmt"
    )
    
    func main() {
        /*
               Go语言切片删除:
                   Go语言并没有对删除切片提供相对应的函数,需要使用切片本身的特性来删除元素。
    
               切片删除的本质:
                    以被删除的元素为起点,到删除的元素为终点,将前后两部分数据在内存重新连接起来。
                    当删除一个切片的第一个元素时,那么第一个元素的存储空间会被释放,但此时后面的所有元素都得集体往前移动一个元素哟,这是很消耗性能的,尤其是在数据量大的情况下;
                    当删除一个切片的最后一个元素时,那么最后一个元素的存储空间也会被释放,由于最后一个元素后面没有元素啦,因此不会产生数据的移动。
    
               温馨提示:
                    如果切片元素过多,整个删除过程非常消耗性能,因为删除切片前面的元素,那么该切片前面的元素空出来后,后面的元素依次往前面移动,如果数据量很大的情况下效率极低。
                    生产环境中,不建议大家对数据量较大的切片进行元素删除操作,如果数据有频繁的删除操作哦,建议换其它数据存储容器。
                    删除切片的数据并不会修改切片的容量大小。
        */
        s1 := []rune{'2', '0', '2', '0', '', '', '', '', ''}
        fmt.Printf("删除前s1的数据为:[%s],长度为:[%d],容量为:[%d],内存地址为:[%p]
    ", string(s1), len(s1), cap(s1), s1)
        fmt.Printf("删除前s1各元素地址:[%p] [%p] [%p] [%p] [%p] [%p] [%p] [%p] [%p]  
    ", &s1[0], &s1[1], &s1[2], &s1[3], &s1[4], &s1[5], &s1[6], &s1[7], &s1[8])
    
        s1 = append(s1[:0], s1[4:]...) //删除下标前面四个的元素,左闭右开(即顾左不顾右),需要使用不定参格式
        fmt.Printf("删除后s1的数据为:[%s],长度为:[%d],容量为:[%d],内存地址为:[%p]
    ", string(s1), len(s1), cap(s1), s1)
        fmt.Printf("删除后s1各元素地址:[%p] [%p] [%p] [%p] [%p] 
    ", &s1[0], &s1[1], &s1[2], &s1[3], &s1[4])
    }

  • 相关阅读:
    10多媒体
    胡凡-01
    概念
    算法
    07Axios
    05VueCli
    04Vue.js路由系统
    03生命周期
    《穷人思维》学习感悟
    《基金》学习感悟之二
  • 原文地址:https://www.cnblogs.com/yinzhengjie2020/p/12329364.html
Copyright © 2011-2022 走看看