Golang语言之切片(slice)快速入门篇

简介: 这篇文章是关于Go语言中切片(slice)的快速入门教程,详细介绍了切片的概念、定义方式、遍历、扩容机制、使用注意事项以及相关练习题。

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

一.切片(slice)概述

1.数组的局限性

数组的三个特点:
    - 1.长度固定;
    - 2.连续内存空间;
    - 3.同一类型集合;

因为数组的长度是固定的并且数组长度属于类型的一部分,所以数组有很多的局限性,比如数组(array)无法实现扩容和缩容。

2.切片(slice)概述


切片(slice)是Golang中一种特有的数据类型,如上图所示, 切片的本质就是对底层数组的封装,它包含了三个信息:
    - 1.底层数组的指针;
    - 2.切片的长度(len);
    - 3.切片的容量(cap);


切片是一个拥有相同类型元素的可变长度的序列。它是基于数组类型做的一层封装。支持自动扩容。切片的三个特点:
    - 1.长度可变;
    - 2.连续内存空间;
    - 3.同一类型集合;

切片是数组一个连续片段的引用,所以切片是一个引用类型,它的内部结构包含地址、长度和容量。切片一般用于快速地操作一块数据集合。

这个片段可以是整个数组,或者由起始和终止索引标识符的一些项的子集,终止索引标识的项不包括在切片内。


切片和数组的区别:
    - 1.切片长度不固定,可以根据需求自动扩容,数组长度是固定的;
    - 2.数组的长度和容量相等切不可变,而切片的长度和容量并不一定相等;

3.切片的内存分析

package main

import "fmt"

func main() {
    // 定义数组
    var intArray [5]uint8 = [5]uint8{1, 3, 5, 7, 9}

    // 切片构建在数组之上,如果基于数组的索引取切片一定要注意口诀: "前包后不包"。
    var slice []uint8 = intArray[1:4]

    fmt.Printf("intArray数组: %v, 长度: %d, 容量: %d\n", intArray, len(intArray), cap(intArray))

    fmt.Printf("slice切片: %v, 长度: %d, 容量: %d\n", slice, len(slice), cap(slice))

    fmt.Printf("intArray[1]数组的内存地址: %p\n", &intArray[1])

    fmt.Printf("slice[0]切片的内存地址: %p\n", &slice[0])
    fmt.Printf("slice[1]切片的内存地址: %p\n", &slice[1])

    // 修改切片的数据
    slice[1] = 88

    // 查看数组和切片的数据是否修改
    fmt.Printf("intArray数组: %v, 长度: %d, 容量: %d\n", intArray, len(intArray), cap(intArray))
    fmt.Printf("slice切片: %v, 长度: %d, 容量: %d\n", slice, len(slice), cap(slice))

}

二.切片的三种定义方式

1.切片表达式(基于已经存在的数组来创建切片)

package main

import "fmt"

func main() {

    /*
            切片表达式从字符串、数组、指向数组或切片的指针构造子字符串或切片。

            切片表达式有两种变体:(省略了low则默认为0,省略了high则默认为切片操作数的长度)
                (1)一种指定low和high两个索引界限值的简单的形式;
                    array[low:high]
                (2)另一种是除了low和high索引界限值外还指定容量的完整的形式;
                    array[low:high:max]

            完整切片表达式需要满足的条件是0 >= low >= high >= max >= cap(a),其他条件和简单切片表达式相同;

            当设置了max时,则切片的容量设置为"max-low";
    */

    a := [5]int{1, 2, 3, 4, 5}

    s1 := a[2:] // 等同于 a[2:len(a)]
    s2 := a[:3] // 等同于 a[0:3]
    s3 := a[:]  // 等同于 a[0:len(a)]

    s4 := a[1:3:5]
    s5 := a[1:3:3]

    fmt.Printf("s1:%v len(s1):%v cap(s1):%v\n", s1, len(s1), cap(s1))
    fmt.Printf("s2:%v len(s2):%v cap(s2):%v\n", s2, len(s2), cap(s2))
    fmt.Printf("s3:%v len(s3):%v cap(s3):%v\n", s3, len(s3), cap(s3))
    fmt.Printf("s4:%v len(s4):%v cap(s4):%v\n", s4, len(s4), cap(s4))
    fmt.Printf("s5:%v len(s5):%v cap(s5):%v\n", s5, len(s5), cap(s5))

}

2.通过make指令创建切片

package main

import "fmt"

func main() {
    /*
        如果需要动态的创建一个切片,我们就需要使用内置的make()函数,格式如下:
            make([]T, size, cap)

        通过make函数常见切片需要传入三个参数:
            T:
                切片的类型
            size:
                切片的长度
            cap:
                切片的容量,容量并不会影响当前元素的个数。 

        make创建切片本质上就是在底层创建了一个数组,该数组对外不可见,所以不可以直接操作这个数组,要通过切片去间接的访问各个元素。
    */
    slice := make([]int, 4, 20)

    // 为切片赋值
    slice[1] = 100
    slice[3] = 200

    fmt.Printf("切片的长度:%d,容量:%d,数据:%v\n", len(slice), cap(slice), slice)

}

3.声明切片类型

package main

import (
    "fmt"
)

func main() {
    // 声明切片类型,定义一个切片,直接就指定具体数组,使用原理类似make
    var (
        // 声明一个字符串切片
        bigdata = []string{"hadoop", "spark", "flink", "kudu", "hbase", "hive"}

        // 声明一个整型切片并初始化
        scores = []int{99, 88, 77}

        // 声明一个布尔切片并初始化
        svip = []bool{false, true}
    )

    fmt.Printf("bigdata切片的长度:[%d],容量:[%d],数据:%v\n", len(bigdata), cap(bigdata), bigdata)
    fmt.Printf("scores切片的长度:[%d],容量:[%d],数据:%v\n", len(scores), cap(scores), scores)
    fmt.Printf("svip切片的长度:[%d],容量:[%d],数据:%v\n", len(svip), cap(svip), svip)
}

三.切片的遍历

1.基于for循环遍历

package main

import (
    "fmt"
)

func main() {
    s := []byte{'A', 'B', 'C'}

    // 支持基于索引遍历
    for i := 0; i < len(s); i++ {
        fmt.Printf("第[%d]个索引存储的数据是: [%c]\n", i, s[i])
    }
}

2.基于for-range循环遍历

package main

import (
    "fmt"
)

func main() {
    s := []byte{'A', 'B', 'C'}

    // 支持基for-range循环
    for index, value := range s {
        fmt.Printf("第[%d]个索引对应的数据为:%c\n", index, value)
    }
}

四.切片的扩容

1.通过append函数扩容切片

package main

import "fmt"

func main() {
    var (
        // 1.定义数组

        intArr [5]int = [5]int{1, 3, 5, 7, 9}
        // 2.定义切片

        s1 []int = intArr[1:4]
    )

    /*
        切片扩容的底层原理:
            - 1.底层追加元素的时候对数组进行扩容,老数组扩容为新数组;
            - 2.创建一个新数组,将老数组中的s1("3","5","7")复制到新数组中,在新数组中追加"66","88";
            - 3.s2底层数组指向的是新数组;
            - 4.往往我们在使用追加的时候起始想要做的效果给s1追加;
            - 5.底层的新数组不能直接维护,需要通过切片简洁维护操作;
    */
    s2 := append(s1, 66, 88)

    fmt.Printf("s1:%v len(s1):%v cap(s1):%v\n", s1, len(s1), cap(s1))
    fmt.Printf("s2:%v len(s2):%v cap(s2):%v\n", s2, len(s2), cap(s2))

    s3 := []int{22, 33, 44}

    // append也支持将一个切片直接追加到另一个切片中
    s1 = append(s1, s3...)

    fmt.Printf("s3:%v len(s3):%v cap(s3):%v\n", s3, len(s3), cap(s3))
    fmt.Printf("s1:%v len(s1):%v cap(s1):%v\n", s1, len(s1), cap(s1))

}

2.切片自动扩容

package main

import (
    "fmt"
)

func main() {

    var numSlice []int
    fmt.Printf("未添加任何元素:%p 数据: %v  长度:%d  容量:%d  \n", numSlice, numSlice, len(numSlice), cap(numSlice))

    for i := 0; i < 10; i++ {
        /*
            温馨提示:
                - 1.每个切片会指向一个底层数组,这个数组的容量够用就添加新增元素。
                - 2.当底层数组不能容纳新增的元素时,切片就会自动按照一定的策略进行“扩容”,此时该切片指向的底层数组就会更换。
                - 3.“扩容”操作往往发生在append()函数调用时,所以我们通常都需要用原变量接收append函数的返回值。
        */
        numSlice = append(numSlice, i)

        fmt.Printf("内存地址指针:%p 数据: %v  长度:%d  容量:%d  \n", numSlice, numSlice, len(numSlice), cap(numSlice))
    }
}

3.切片的扩容策略[了解即可]

可以通过查看$GOROOT/src/runtime/slice.go源码,其中扩容相关代码如下:
    newcap := old.cap
    doublecap := newcap + newcap
    if cap > doublecap {
        newcap = cap
    } else {
        if old.len < 1024 {
            newcap = doublecap
        } else {
            // Check 0 < newcap to detect overflow
            // and prevent an infinite loop.
            for 0 < newcap && newcap < cap {
                newcap += newcap / 4
            }
            // Set newcap to the requested cap when
            // the newcap calculation overflowed.
            if newcap <= 0 {
                newcap = cap
            }
        }
    }


从上面的代码可以看出以下内容:
    - 1.首先判断,如果新申请容量(cap)大于2倍的旧容量(old.cap),最终容量(newcap)就是新申请的容量(cap),否则走else语句继续判断;

    - 2.如果旧切片的长度小于1024,则最终容量(newcap)就是旧容量(old.cap)的两倍,即(newcap=doublecap),否则走else语句继续判断;

    - 3.如果旧切片长度大于等于1024,则最终容量(newcap)从旧容量(old.cap)开始循环增加原来的1/4,即(newcap=old.cap,for {newcap += newcap/4})直到最终容量(newcap)大于等于新申请的容量(cap),即(newcap >= cap);

    - 5.如果最终容量(cap)计算值溢出,则最终容量(cap)就是新申请容量(cap);



温馨提示:
    切片扩容还会根据切片中元素的类型不同而做不同的处理,比如int和string类型的处理方式就不一样。

五.切片使用注意事项

1.切片使用注意事项

- 1.切片定义后不可以直接使用,需要让其引用到一个数组,或者make一个空间供切片来使用;

- 2.切片使用不能越界;

- 3.切片表达式支持简写形式
    案例1: 
        "var s1 = arr[0:end]"简写为:"var s1 = arr[:end]"

    案例2:
        "var s2 = arr[start:len(arr)]"简写为"var s2 = arr[start:]"

    案例3:
        "var s3 = arr[0:len(arr)]简写为"var s3 = arr[:]"

- 4.切片可以继续切片;

- 5.切片可以动态增长,通过"append"函数操作即可;

- 6.切片也支持使用内置函数copy进行拷贝;

- 7.切片是引用类型,不支持直接比较,切片唯一合法的比较操作是和nil比较

1.切片的赋值拷贝

package main

import (
    "fmt"
)

func main() {
    s1 := make([]int, 3)

    // 切片是引用类型,将s1直接复制给s2,此时s1和s2底层共用同一个数组
    s2 := s1

    fmt.Printf("修改前: s1 --->[%v]\n", s1)
    fmt.Printf("修改前: s2 --->[%v]\n", s2)

    // 注意,我修改的是s2,并没有取修改s1哈
    s2[1] = 100

    // 再次查看s1和s2时你会发现二者都发生了变化哟
    fmt.Printf("修改后: s1 --->[%v]\n", s1)
    fmt.Printf("修改后: s2 --->[%v]\n", s2)

}

2.切片的复制

package main

import (
    "fmt"
)

func main() {
    s1 := []int{11, 22, 33, 44, 55}
    s2 := s1

    // 切片是引用类型,所以s1和s2其实都指向了同一块内存地址。修改s2的同时s1的值也会发生变化。
    s2[1] = 100
    fmt.Printf("修改前: s1 ---> 内存地址: %p [%v], \n", s1, s1)
    fmt.Printf("修改前: s2 ---> 内存地址: %p [%v]\n", s2, s2)

    // 注意,使用make函数对切片进行初始化操作,此操作在"go1.19.3"版本中对s1和s2指向的数组值也有影响哟~
    s3 := make([]int, 3)

    // Go语言内建的copy()函数可以迅速地将一个切片的数据复制到另外一个切片空间中,copy()函数的使用格式如下:
    //         copy(destSlice, srcSlice []T)
    //
    // 温馨提示:
    //        srcSlice:
    //            数据来源切片
    //         destSlice:
    //            目标切片
    copy(s1, s3) // 使用copy()函数将切片s1中的元素复制到切片s3,属于值拷贝。

    // 由于s3属于只是使用了内置函数copy对s1进行了值拷贝,因此修改s3并不会影响到s1和s2哟~
    s3[2] = 200
    fmt.Printf("s1 ---> 内存地址: %p 数据: %v 长度: %d 容量: %d\n", s1, s1, len(s1), cap(s1))
    fmt.Printf("s2 ---> 内存地址: %p 数据: %v 长度: %d 容量: %d\n", s2, s2, len(s2), cap(s2))
    fmt.Printf("s3 ---> 内存地址: %p 数据: %v 长度: %d 容量: %d\n", s3, s3, len(s3), cap(s3))
}

3.切片元素删除

package main

import (
    "fmt"
)

func main() {

    numberList := []int{11, 22, 33, 44, 55}
    fmt.Printf("删除前 ---> numberList: %v\n", numberList)

    // Go语言中并没有删除切片元素的专用方法,我们可以使用切片本身的特性来删除元素。
    // 要从切片a中删除索引为index的元素,操作方法是a = append(a[:index], a[index+1:]...)
    numberList = append(numberList[:3], numberList[4:]...) // 删除索引为3的元素

    fmt.Printf("删除后 ---> numberList: %v\n", numberList)
}

4.切片不能直接比较

package main

import (
    "fmt"
)

func main() {
    var a = []bool{false, true}
    var b = []bool{false, true}

    fmt.Printf("a ---> %v\n", a)
    fmt.Printf("b ---> %v\n", b)

    // 切片是引用类型,不支持直接比较,切片唯一合法的比较操作是和nil比较。
    // fmt.Println(a == b) //  报错: invalid operation: a == b (slice can only be compared to nil)

}

5.判断切片是否为空

package main

import (
    "fmt"
)

func main() {
    var s1 []int
    s2 := []int{}
    s3 := make([]int, 0)
    fmt.Printf("s1:%v len(s1):%v cap(s1):%v\n", s1, len(s1), cap(s1))
    fmt.Printf("s2:%v len(s2):%v cap(s2):%v\n", s2, len(s2), cap(s2))
    fmt.Printf("s3:%v len(s3):%v cap(s3):%v\n", s3, len(s3), cap(s3))

    // 温馨提示:
    //         (1)一个nil值的切片并没有底层数组,一个nil值的切片的长度和容量一定都是0;
    //         (2)我们不能说一个长度和容量都是0的切片一定是nil;
    fmt.Printf("s1是否为空: %t\n", s1 == nil)
    fmt.Printf("s2是否为空: %t\n", s2 == nil)
    fmt.Printf("s3是否为空: %t\n", s3 == nil)

    // 综上所述,要判断一个切片是否是空的,要是用len(s) == 0来判断,不应该使用s == nil来判断。
    fmt.Println(len(s1) == 0)
    fmt.Println(len(s2) == 0)
    fmt.Println(len(s3) == 0)

}

六.练习题

1.观看代码手写运行结果

package main

import (
    "fmt"
)

func main() {
    var a = make([]string, 5, 10)
    fmt.Printf("数据: %v 长度: %d 容量: %d\n", a, len(a), cap(a))

    for i := 0; i < 10; i++ {
        a = append(a, fmt.Sprintf("%v", i))
    }

    fmt.Printf("数据: %v 长度: %d 容量: %d\n", a, len(a), cap(a))
}

2.使用sort包对数组进行排序

package main

import (
    "fmt"
    "sort"
)

func main() {

    var a = [...]int{3, 7, 8, 9, 1}
    fmt.Printf("排序前: %v\n", a)

    sort.Ints(a[:]) // 对切片进行排序,该切片底层对应的就是上面的可变数组哟~
    fmt.Printf("排序后: %v\n", a)
}
目录
相关文章
|
14天前
|
Go
Golang语言之管道channel快速入门篇
这篇文章是关于Go语言中管道(channel)的快速入门教程,涵盖了管道的基本使用、有缓冲和无缓冲管道的区别、管道的关闭、遍历、协程和管道的协同工作、单向通道的使用以及select多路复用的详细案例和解释。
47 4
Golang语言之管道channel快速入门篇
|
14天前
|
Go
Golang语言之gRPC程序设计示例
这篇文章是关于Golang语言使用gRPC进行程序设计的详细教程,涵盖了RPC协议的介绍、gRPC环境的搭建、Protocol Buffers的使用、gRPC服务的编写和通信示例。
42 3
Golang语言之gRPC程序设计示例
|
14天前
|
安全 Go
Golang语言goroutine协程并发安全及锁机制
这篇文章是关于Go语言中多协程操作同一数据问题、互斥锁Mutex和读写互斥锁RWMutex的详细介绍及使用案例,涵盖了如何使用这些同步原语来解决并发访问共享资源时的数据安全问题。
37 4
|
14天前
|
Go 调度
Golang语言goroutine协程篇
这篇文章是关于Go语言goroutine协程的详细教程,涵盖了并发编程的常见术语、goroutine的创建和调度、使用sync.WaitGroup控制协程退出以及如何通过GOMAXPROCS设置程序并发时占用的CPU逻辑核心数。
23 4
Golang语言goroutine协程篇
|
14天前
|
Prometheus Cloud Native Go
Golang语言之Prometheus的日志模块使用案例
这篇文章是关于如何在Golang语言项目中使用Prometheus的日志模块的案例,包括源代码编写、编译和测试步骤。
20 3
Golang语言之Prometheus的日志模块使用案例
|
14天前
|
Go
Golang语言文件操作快速入门篇
这篇文章是关于Go语言文件操作快速入门的教程,涵盖了文件的读取、写入、复制操作以及使用标准库中的ioutil、bufio、os等包进行文件操作的详细案例。
37 4
Golang语言文件操作快速入门篇
|
14天前
|
Go
Golang语言错误处理机制
这篇文章是关于Golang语言错误处理机制的教程,介绍了使用defer结合recover捕获错误、基于errors.New自定义错误以及使用panic抛出自定义错误的方法。
36 3
|
14天前
|
Go
Golang语言之函数(func)进阶篇
这篇文章是关于Golang语言中函数高级用法的教程,涵盖了初始化函数、匿名函数、闭包函数、高阶函数、defer关键字以及系统函数的使用和案例。
16 3
Golang语言之函数(func)进阶篇
|
14天前
|
Go
Golang语言之函数(func)基础篇
这篇文章深入讲解了Golang语言中函数的定义和使用,包括函数的引入原因、使用细节、定义语法,并通过多个案例展示了如何定义不返回任何参数、返回一个或多个参数、返回值命名、可变参数的函数,同时探讨了函数默认值传递、指针传递、函数作为变量和参数、自定义数据类型以及返回值为切片类型的函数。
17 2
Golang语言之函数(func)基础篇
|
14天前
|
Go
Golang语言之映射(map)快速入门篇
这篇文章是关于Go语言中映射(map)的快速入门教程,涵盖了map的定义、创建方式、基本操作如增删改查、遍历、嵌套map的使用以及相关练习题。
21 5