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])
    }

  • 相关阅读:
    Oracle Core 学习笔记二 Transactions 和 Consistency 说明
    Oracle AUTO_SPACE_ADVISOR_JOB 说明
    Windows 下 ftp 上传文件 脚本
    Oracle 11g 中 Direct path reads 特性 说明
    Linux 使用 wget 下载 Oracle 软件说明
    Oracle 10g read by other session 等待 说明
    Oracle 11g RAC INS06006 Passwordless SSH connectivity not set up between the following node(s) 解决方法
    SecureCRT 工具 上传下载数据 与 ASCII、Xmodem、Ymodem 、Zmodem 说明
    Oracle RAC root.sh 报错 Timed out waiting for the CRS stack to start 解决方法
    Oracle RESETLOGS 和 NORESETLOGS 区别说明
  • 原文地址:https://www.cnblogs.com/yinzhengjie2020/p/12329364.html
Copyright © 2011-2022 走看看