Go 学习笔记(11):切片

栏目: Go · 发布时间: 6年前

内容简介:切片(一个

切片( slice )是对数组一个连续片段的引用,,所以切片是一个引用类型(更类似于 C/C++ 中的数组, Python 中的 list )。因为切片是引用,不需要使用额外的内存存储并且比数组更有效率,所以在 Go 中 切片比数组更常用。

构成

一个 slice 由三个部分构成:指针、长度和容量:

  • 指针:指向第一个 slice 元素对应的底层数组元素的地址(注意, slice 的第一个元素并不一定就是数组的第一个元素)
  • 长度对应 slice 中元素的数目,不能超过容量
  • 容量一般是从 slice 的开始位置到底层数组的结尾位置,内置的 lencap 函数分别返回 slice 的长度和容量。
Go 学习笔记(11):切片

创建

数组切片

Slice 本身没有数据,是对底层数组的 view 。多个 slice 之间可以共享底层的数据,并且引用的数组部分区间可能重叠,即一个切片和相关数组的其他切片是共享存储的。相反,不同的数组总是代表不同的存储(数组实际上是切片的构建块)。

arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7}  // [0 1 2 3 4 5 6 7]
s := arr[2:6]                            // [2 3 4 5]

// 共享存储
s[0] = 10
fmt.Println(arr)                         // [0 1 10 3 4 5 6 7]
fmt.Println((s))                         // [10 3 4 5]
复制代码

数组和 slice 之间有着紧密的联系,和数组不同的是,切片的长度可以在运行时修改,最小为 0 最大为相关数组的长度,切片是一个 长度可变的数组。

声明赋值

切片与数组的类型字面量的唯一不同是不包含代表其长度的信息。因此,不同长度的切片值是有可能属于同一个类型的,不同长度的数组值必定属于不同类型。

s := []int{1, 2, 3} 
复制代码

使用append

由于值传递的关系,必须接收 append 的返回值:

s = append(s, val)
s = append(s, val1,val2, val3)
s = append(s, slice...)
复制代码
var s []int
for i :=0; i<10; i++ {
  s = append(s, i)
}
fmt.Println(s)
复制代码

使用make

cap 是可选参数: make([]type, len, cap)

s1 := make([]int, 16)
s2 := make([]int, 10, 32)
复制代码

扩展阅读 —— new()make() 的区别 : 两者都在堆上分配内存,但是它们的行为不同,适用于不同的类型:

  • new(T) 为类型 T 分配一片内存,初始化为 0 并且返回类型为 *T 的内存地址,返回一个指向类型为 T ,值为 0 的地址的 指针 。适用于数组、结构体。
  • make(T) 返回一个类型为 T 的初始值,它只适用于 3 种内建的引用类型:切片、 mapchannel

也就是说, new 函数分配内存, make 函数初始化。

特性

长度len

切片元素的个数,使用内置函数 len 获取。

容量cap

数组的容量是其长度,切片的容量是切片的第一个元素到底层数组的最后一个元素的长度。

如果切片操作超出 cap(s) 的上限将导致一个 panic 异常,但是超出 len(s) 则是意味着扩展了 slice ,新 slice 的长度会变大(请见添加元素 - 自动扩展 cap )。

// 数组的容量是其长度
arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7}
fmt.Println(cap(arr))  // 8

// 切片的容量
arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7}
s1 := arr[2:6]        // [2 3 4 5]
s2 := s1[3:5]         // [5 6],注意,扩展了
fmt.Println(cap(s1))  // 6
fmt.Println(cap(s2))  // 3
复制代码

操作

添加元素

arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7}
s1 := arr[2:6]
s2 := s1[3:5]
s3 := append(s2, 10)
s4 := append(s3, 11)
s5 := append(s4, 12)
fmt.Println(s1)    // [2 3 4 5]
fmt.Println(s2)    // [5 6],注意,扩展了s1
fmt.Println(s3)    // [5 6 10]
fmt.Println(s4)    // [5 6 10 11]
fmt.Println(s5)    // [5 6 10 11 12]
fmt.Println(arr)   // [0 1 2 3 4 5 6 10]
复制代码

观察最后的 arr , 长度仍然是 7 ,说明 s3append 改变了原 arrays4s5append 由于超出了底层数组的容量,实际上 s3s4 不再 viewarr 了,而是 view 了个新的更加长的 array

可见, 添加元素时如果超越了 cap,系统会重新分配更大的底层数组 。如果原数组不再被使用,会被垃圾回收。

自动扩展 cap:

var s []int

for i :=0; i<10; i++ {
    s = append(s, i)
    fmt.Println(len(s), cap(s))
}
fmt.Println(s)

// 1 1
// 2 2
// 3 4
// 4 4
// 5 8
// 6 8
// 7 8
// 8 8
// 9 16
// 10 16
// [0 1 2 3 4 5 6 7 8 9]
复制代码

删除元素

删除索引为 3 的元素,也可以使用 copy 实现,见模拟 stack

arr := [...]int{0, 1, 2, 3, 4, 5}
s := arr[:]     // [0 1 2 3 4 5]

s = append(s[:3], s[4:]...)
fmt.Println(s)  // [0 1 2 4 5]
复制代码

切片拷贝

copy(dst slice, src slice) :对第一个参数值进行修改。

两个 slice 可以共享同一个底层数组,甚至有重叠也没有问题。 copy 函数将返回成功复制的元素的个数,等于两个 slice 中较小的长度,所以我们不用担心覆盖会超出目标 slice 的范围。

s1 := []int{1, 2}
s2 := []int{4, 5, 6}
copy(s1, s2)
fmt.Println(s1)  // [4 5]

s1 := []int{1, 1, 1, 1}
s2 := []int{4, 5, 6}
copy(s1, s2)
fmt.Println(s1)  // [4 5 6 1]
复制代码

遍历

for-range 结构遍历切片

比较

和数组不同的是, slice 之间不能比较,因此我们不能使用 == 操作符来判断两个 slice 是否含有全部相等元素。

标准库提供了高度优化的 bytes.Equal 函数来判断两个 字节型 slice 是否相等( []byte ),但是对于其他类型的 slice ,我们必须自己展开每个元素进行比较:

func equal(x, y []string) bool {
    if len(x) != len(y) {
        return false
    }
    for i := range x {
        if x[i] != y[i] {
            return false
        }
    }
    return true
}
复制代码

nil

slice 唯一合法的比较操作是和 nil 比较:

if slice1 == nil { /* ... */ }
复制代码
  • 一个零值的 slice 等于 nil
  • 一个 nil 值的 slice 并没有底层数组
  • 一个 nil 值的 slice 的长度和容量都是 0 ,但是也有非 nil 值的 slice 的长度和容量也是 0 的,例如 []int{}make([]int, 3)[3:]
  • 一个 nil 值的 slice 的行为和其它任意 0 长度的 slice 一样,所有 Go 语言函数应该同等式对待 nil 值的 slice0 长度的 slice
  • 如果需要测试一个 slice 是否是空的,使用 len(s) == 0 来判断,不要用 s == nil 来判断
var s1 []int
fmt.Println(len(s1), s1 == nil)    // 0 true

s2 := []int{}
fmt.Println(len(s2), s2 == nil)    // 0 false

s3 := []int(nil)
fmt.Println(len(s3), s3 == nil)    // 0 true
复制代码

函数传参

如果一个函数需要对数组操作,最好把参数声明为切片。当调用函数时,把数组分片,传入 切片引用

数组元素和

package main

import "fmt"

func sum(a []int) int {
    s := 0
    for i := range a {
        s += i
    }
    return s
}

func main() {
    arr := [...]int{0, 1, 2, 3, 4, 5}
    fmt.Println(sum(arr[:]))          // 15

    var s1 []int
    fmt.Println(s1 == nil, sum(s1))   // true 0

    s2 := []int{}
    fmt.Println(s2 == nil, sum(s2))   // false 0
}

复制代码

反转数组

package main

import "fmt"

func reverse(s []int) {
    // for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
    //     s[i], s[j] = s[j], s[i]
    // }

    for index, i := range s {
        s[index], s[len(s) - i - 1] = s[len(s) - i - 1], s[index]
    }
}

func main() {
    arr := [...]int{0, 1, 2, 3, 4, 5}
    reverse(arr[:])
    fmt.Println(arr)

    var s1 []int
    reverse(s1)
    fmt.Println(s1)

    s2 := []int{}
    reverse(s2)
    fmt.Println(s2)
}
复制代码

应用技巧

去除空值

举个例子,去除切片中的空值。注意,输入的 slice 和输出的 slice 共享同一底层数组,这有可能修改了原来的数组。这是重用原来的 slice ,节约了内存。

package main

import (
    "fmt"
)

func nonempty(strings []string) []string {
    i := 0
    for _, s := range strings {
        if s != "" {
            strings[i] = s
            i++
        }
    }
    return strings[:i]
}

func main() {
    data := []string{"one", "", "three"}
    fmt.Printf("%q\n", nonempty(data))  // ["one" "three"]
    fmt.Printf("%q\n", data)            // ["one" "three" "three"],改变原数组
}
复制代码

使用 append 函数实现:

func nonempty2(strings []string) []string {
    out := strings[:0]  // 注意,重用原 slice 的关键
    for _, s := range strings {
        if s != "" {
            out = append(out, s)
        }
    }
    return out
}
复制代码

模拟栈

上面的 nonempty2 函数是用 slice 模拟一个 stack ,最初给定的空 slice 对应一个空的 stack

插入元素( push ):

stack = append(stack, v)
复制代码

取出最顶部( slice 的最后)的元素:

top := stack[len(stack)-1]
复制代码

通过收缩 stack 弹出栈顶的元素( pop ):

stack = stack[:len(stack)-1] // pop
复制代码

删除 slice 中间的某个元素并保存原有的元素顺序:

func remove(slice []int, i int) []int {
    copy(slice[i:], slice[i+1:])
    return slice[:len(slice)-1]
    // 或者
    // return append(slice[:i], slice[i+1:]...)
}
复制代码

深入理解

appendInt

append 函数对于理解 slice 底层是如何工作的非常重要。下面是第一个版本的 appendInt 函数,专门用于处理 []int 类型的 slice

func appendInt(x []int, y int) []int {
    var z []int
    zlen := len(x) + 1
    if zlen <= cap(x) {
        // There is room to grow.  Extend the slice.
        z = x[:zlen]
    } else {
        // There is insufficient space.  Allocate a new array.
        // Grow by doubling, for amortized linear complexity.
        zcap := zlen
        if zcap < 2*len(x) {
            zcap = 2 * len(x)
        }
        z = make([]int, zlen, zcap)
        copy(z, x) // a built-in function; see text
    }
    z[len(x)] = y
    return z
}
复制代码
  1. 先检测 slice 底层数组是否有足够的容量来保存新添加的元素
  2. 如果空间足够,直接在原有底层数组之上扩展 slice ,将新添加的 y 元素复制到新扩展的空间,并返回 slice 。可见,输入的 x 和输出的 z 共享相同的底层数组
  3. 如果没有足够的增长空间, appendInt 函数则会先分配一个足够大的 slice 用于保存新的结果,先将输入的 x 复制到新的空间,然后添加 y 元素。结果 z 和输入的 x 引用的将是不同的底层数组
  4. 为了提高内存使用效率,新分配的数组一般略大于保存 xy 所需要的最低大小。通过在每次扩展数组时直接将长度翻倍从而避免了多次内存分配,也确保了添加单个元素操的平均时间是一个常数时间。

append

内置的 append 函数可能比 appendInt 的内存扩展策略更复杂,通常我们并不知道 append 调用是否导致了内存的重新分配,所以也不能确认新的 slice 和原始的 slice 是否引用的是相同的底层数组空间。此时,我们就不能确认在原先的 slice 上的操作是否会影响到新的 slice 。在这种情况下,通常是将 append 返回的结果直接赋值给输入的 slice 变量: s = append(s, val)

更新 slice 变量不仅对调用 append 函数是必要的,实际上对应任何可能导致长度、容量或底层数组变化的操作都是必要的。要正确地使用 slice ,需要记住尽管底层数组的元素是间接访问的,但是 slice 对应结构体本身的指针、长度和容量部分是直接访问的。要更新这些信息需要像上面例子那样一个显式的赋值操作。从这个角度看, slice 并不是一个纯粹的引用类型,它实际上是一个类似下面结构体的聚合类型:

type IntSlice struct {
    ptr      *int
    len, cap int
}
复制代码

内置的 append 函数可以追加多个元素,甚至追加一个 slice :

var x []int
x = append(x, 1)
x = append(x, 2, 3)
x = append(x, 4, 5, 6)
x = append(x, x...) // append the slice x
复制代码

修改 appendInt ,实现类似 append 函数的功能:

func appendInt(x []int, y ...int) []int {
    var z []int
    zlen := len(x) + len(y)
    // ...expand z to at least zlen...
    copy(z[len(x):], y)
    return z
}
复制代码

参考目录


以上所述就是小编给大家介绍的《Go 学习笔记(11):切片》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

The Everything Store

The Everything Store

Brad Stone / Little, Brown and Company / 2013-10-22 / USD 28.00

The definitive story of Amazon.com, one of the most successful companies in the world, and of its driven, brilliant founder, Jeff Bezos. Amazon.com started off delivering books through the mail. Bu......一起来看看 《The Everything Store》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码