修改数组的类型_Go 数组与切片

429e6771ee08fed895b1ef1834b89993.png

1 声明和初始化

1.1 概念

数组是具有相同 唯一类型 的一组已编号且长度固定的数据项序列(这是一种同构的数据结构);这种类型可以是任意的原始类型例如整形、字符串或者自定义类型。数组长度必须是一个常量表达式,并且必须是一个非负整数。数组长度也是数组类型的一部分,所以[5]int和[10]int是属于不同类型的。数组的编译时值初始化是按照数组顺序完成的(如下)。

注意事项 如果我们想让数组元素类型为任意类型的话可以使用空接口作为类型(参考 第 11 章)。当使用值时我们必须先做一个类型判断(参考 第 11 章)。

数组元素可以通过 索引(位置)来读取(或者修改),索引从 0 开始,第一个元素索引为 0,第二个索引为 1,以此类推。(数组以 0 开始在所有类 C 语言中是相似的)。元素的数目,也称为长度或者数组大小必须是固定的并且在声明该数组时就给出(编译时需要知道数组长度以便分配内存);数组长度最大为 2Gb。

声明的格式是:

var identifier [len]type

例如:

var arr1 [5]int

在内存中的结构是:

288264db19f8ebd513bf2200fa533d74.png

每个元素是一个整形值,当声明数组时所有的元素都会被自动初始化为默认值 0。

arr1 的长度是 5,索引范围从 0 到 len(arr1)-1

第一个元素是 arr1[0],第三个元素是 arr1[2];总体来说索引 i 代表的元素是 arr1[i],最后一个元素是 arr1[len(arr1)-1]

对索引项为 i 的数组元素赋值可以这么操作:arr[i] = value,所以数组是 可变的

只有有效的索引可以被使用,当使用等于或者大于 len(arr1) 的索引时:如果编译器可以检测到,会给出索引超限的提示信息;如果检测不到的话编译会通过而运行时会 panic:(参考 第 13 章)

runtime error: index out of range

由于索引的存在,遍历数组的方法自然就是使用 for 结构:

  • 通过 for 初始化数组项
  • 通过 for 打印数组元素
  • 通过 for 依次处理元素

示例 1 for_arrays.go

package main
import "fmt"

func main() {
    var arr1 [5]int

    for i:=0; i < len(arr1); i++ {
        arr1[i] = i * 2
    }

    for i:=0; i < len(arr1); i++ {
        fmt.Printf("Array at index %d is %dn", i, arr1[i])
    }
}

输出结果:

Array at index 0 is 0
Array at index 1 is 2
Array at index 2 is 4
Array at index 3 is 6
Array at index 4 is 8

for 循环中的条件非常重要:i < len(arr1),如果写成 i <= len(arr1) 的话会产生越界错误。

IDIOM:

for i:=0; i < len(arr1); i++{
    arr1[i] = ...
}

也可以使用 for-range 的生成方式:

IDIOM:

for i,_:= range arr1 {
...
}

在这里i也是数组的索引。当然这两种 for 结构对于切片(slices)(参考 第 7 章)来说也同样适用。

问题 1 下面代码段的输出是什么?

a := [...]string{"a", "b", "c", "d"}
for i := range a {
    fmt.Println("Array item", i, "is", a[i])
}

Go 语言中的数组是一种 值类型(不像 C/C++ 中是指向首元素的指针),所以可以通过 new() 来创建: var arr1 = new([5]int)

那么这种方式和 var arr2 [5]int 的区别是什么呢?arr1 的类型是 *[5]int,而 arr2的类型是 [5]int

这样的结果就是当把一个数组赋值给另一个时,需要在做一次数组内存的拷贝操作。例如:

arr2 := arr1
arr2[2] = 100

这样两个数组就有了不同的值,在赋值后修改 arr2 不会对 arr1 生效。

所以在函数中数组作为参数传入时,如 func1(arr2),会产生一次数组拷贝,func1 方法不会修改原始的数组 arr2。

如果你想修改原数组,那么 arr2 必须通过&操作符以引用方式传过来,例如 func1(&arr2),下面是一个例子

示例 2 pointer_array.go:

package main
import "fmt"
func f(a [3]int) { fmt.Println(a) }
func fp(a *[3]int) { fmt.Println(a) }

func main() {
    var ar [3]int
    f(ar)     // passes a copy of ar
    fp(&ar) // passes a pointer to ar
}

输出结果:

[0 0 0]
&[0 0 0]

2 切片

2.1 概念

切片(slice)是对数组一个连续片段的引用(该数组我们称之为相关数组,通常是匿名的),所以切片是一个引用类型(因此更类似于 C/C++ 中的数组类型,或者 Python 中的 list 类型)。这个片段可以是整个数组,或者是由起始和终止索引标识的一些项的子集。需要注意的是,终止索引标识的项不包括在切片内。切片提供了一个相关数组的动态窗口。

切片是可索引的,并且可以由 len() 函数获取长度。

给定项的切片索引可能比相关数组的相同元素的索引小。和数组不同的是,切片的长度可以在运行时修改,最小为 0 最大为相关数组的长度:切片是一个 长度可变的数组

切片提供了计算容量的函数 cap() 可以测量切片最长可以达到多少:它等于切片的长度 + 数组除切片之外的长度。如果 s 是一个切片,cap(s) 就是从 s[0] 到数组末尾的数组长度。切片的长度永远不会超过它的容量,所以对于 切片 s 来说该不等式永远成立:0 <= len(s) <= cap(s)

多个切片如果表示同一个数组的片段,它们可以共享数据;因此一个切片和相关数组的其他切片是共享存储的,相反,不同的数组总是代表不同的存储。数组实际上是切片的构建块。

优点 因为切片是引用,所以它们不需要使用额外的内存并且比使用数组更有效率,所以在 Go 代码中 切片比数组更常用。

声明切片的格式是: var identifier []type(不需要说明长度)。

一个切片在未初始化之前默认为 nil,长度为 0。

切片的初始化格式是:var slice1 []type = arr1[start:end]

这表示 slice1 是由数组 arr1 从 start 索引到 end-1 索引之间的元素构成的子集(切分数组,start:end 被称为 slice 表达式)。所以 slice1[0] 就等于 arr1[start]。这可以在 arr1 被填充前就定义好。

如果某个人写:var slice1 []type = arr1[:] 那么 slice1 就等于完整的 arr1 数组(所以这种表示方式是 arr1[0:len(arr1)] 的一种缩写)。另外一种表述方式是:slice1 = &arr1

arr1[2:]arr1[2:len(arr1)] 相同,都包含了数组从第三个到最后的所有元素。

arr1[:3]arr1[0:3] 相同,包含了从第一个到第三个元素(不包括第三个)。

如果你想去掉 slice1 的最后一个元素,只要 slice1 = slice1[:len(slice1)-1]

一个由数字 1、2、3 组成的切片可以这么生成:s := [3]int{1,2,3} 甚至更简单的 s := []int{1,2,3}

s2 := s[:] 是用切片组成的切片,拥有相同的元素,但是仍然指向相同的相关数组。

一个切片 s 可以这样扩展到它的大小上限:s = s[:cap(s)],如果再扩大的话就会导致运行时错误(参见第 7 节)。

对于每一个切片(包括 string),以下状态总是成立的:

s == s[:i] + s[i:] // i是一个整数且: 0 <= i <= len(s)
len(s) < cap(s)

切片也可以用类似数组的方式初始化:var x = []int{2, 3, 5, 7, 11}。这样就创建了一个长度为 5 的数组并且创建了一个相关切片。

切片在内存中的组织方式实际上是一个有 3 个域的结构体:指向相关数组的指针,切片 长度以及切片容量。下图给出了一个长度为 2,容量为 4 的切片。

  • y[0] = 3y[1] = 5
  • 切片 y[0:4] 由 元素 3, 5, 7 和 11 组成。

78f748937058c83344e44fd9313d7bac.png

示例 7 array_slices.go

package main
import "fmt"

func main() {
    var arr1 [6]int
    var slice1 []int = arr1[2:5] // item at index 5 not included!

    // load the array with integers: 0,1,2,3,4,5
    for i := 0; i < len(arr1); i++ {
        arr1[i] = i
    }

    // print the slice
    for i := 0; i < len(slice1); i++ {
        fmt.Printf("Slice at %d is %dn", i, slice1[i])
    }

    fmt.Printf("The length of arr1 is %dn", len(arr1))
    fmt.Printf("The length of slice1 is %dn", len(slice1))
    fmt.Printf("The capacity of slice1 is %dn", cap(slice1))

    // grow the slice
    slice1 = slice1[0:4]
    for i := 0; i < len(slice1); i++ {
        fmt.Printf("Slice at %d is %dn", i, slice1[i])
    }
    fmt.Printf("The length of slice1 is %dn", len(slice1))
    fmt.Printf("The capacity of slice1 is %dn", cap(slice1))

    // grow the slice beyond capacity
    //slice1 = slice1[0:7 ] // panic: runtime error: slice bound out of range
}

输出:

Slice at 0 is 2  
Slice at 1 is 3  
Slice at 2 is 4  
The length of arr1 is 6  
The length of slice1 is 3  
The capacity of slice1 is 4  
Slice at 0 is 2  
Slice at 1 is 3  
Slice at 2 is 4  
Slice at 3 is 5  
The length of slice1 is 4  
The capacity of slice1 is 4

如果 s2 是一个 slice,你可以将 s2 向后移动一位 s2 = s2[1:],但是末尾没有移动。切片只能向后移动,s2 = s2[-1:] 会导致编译错误。切片不能被重新分片以获取数组的前一个元素。

注意 绝对不要用指针指向 slice。切片本身已经是一个引用类型,所以它本身就是一个指针!!

问题 2: 给定切片 b:= []byte{'g', 'o', 'l', 'a', 'n', 'g'},那么 b[1:4]b[:2]b[2:]b[:] 分别是什么?

2.2 将切片传递给函数

如果你有一个函数需要对数组做操作,你可能总是需要把参数声明为切片。当你调用该函数时,把数组分片,创建为一个 切片引用并传递给该函数。这里有一个计算数组元素和的方法:

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

func main() {
    var arr = [5]int{0, 1, 2, 3, 4}
    sum(arr[:])
}

2.3 用 make() 创建一个切片

当相关数组还没有定义时,我们可以使用 make() 函数来创建一个切片 同时创建好相关数组:var slice1 []type = make([]type, len)

也可以简写为 slice1 := make([]type, len),这里 len 是数组的长度并且也是 slice 的初始长度。

所以定义 s2 := make([]int, 10),那么 cap(s2) == len(s2) == 10

make 接受 2 个参数:元素的类型以及切片的元素个数。

如果你想创建一个 slice1,它不占用整个数组,而只是占用以 len 为个数个项,那么只要:slice1 := make([]type, len, cap)

make 的使用方式是:func make([]T, len, cap),其中 cap 是可选参数。

所以下面两种方法可以生成相同的切片:

make([]int, 50, 100)
new([100]int)[0:50]

下图描述了使用 make 方法生成的切片的内存结构:

78f748937058c83344e44fd9313d7bac.png

示例 8 make_slice.go

package main
import "fmt"

func main() {
    var slice1 []int = make([]int, 10)
    // load the array/slice:
    for i := 0; i < len(slice1); i++ {
        slice1[i] = 5 * i
    }

    // print the slice:
    for i := 0; i < len(slice1); i++ {
        fmt.Printf("Slice at %d is %dn", i, slice1[i])
    }
    fmt.Printf("nThe length of slice1 is %dn", len(slice1))
    fmt.Printf("The capacity of slice1 is %dn", cap(slice1))
}

输出:

Slice at 0 is 0  
Slice at 1 is 5  
Slice at 2 is 10  
Slice at 3 is 15  
Slice at 4 is 20  
Slice at 5 is 25  
Slice at 6 is 30  
Slice at 7 is 35  
Slice at 8 is 40  
Slice at 9 is 45  
The length of slice1 is 10  
The capacity of slice1 is 10

因为字符串是纯粹不可变的字节数组,它们也可以被切分成 切片。

练习 4: fobinacci_funcarray.go: 为练习 3 写一个新的版本,主函数调用一个使用序列个数作为参数的函数,该函数返回一个大小为序列个数的 Fibonacci 切片。

2.4 new() 和 make() 的区别

看起来二者没有什么区别,都在堆上分配内存,但是它们的行为不同,适用于不同的类型。

  • new(T) 为每个新的类型T分配一片内存,初始化为 0 并且返回类型为*T的内存地址:这种方法 返回一个指向类型为 T,值为 0 的地址的指针,它适用于值类型如数组和结构体(参见第 10 章);它相当于 &T{}
  • make(T) 返回一个类型为 T 的初始值,它只适用于3种内建的引用类型:切片、map 和 channel(参见第 8 章,第 13 章)。

换言之,new 函数分配内存,make 函数初始化;下图给出了区别:

a2e90c02b3d4bded595092eadb3f5533.png

在图 3 的第一幅图中:

var p *[]int = new([]int) // *p == nil; with len and cap 0
p := new([]int)

在第二幅图中, p := make([]int, 0) ,切片 已经被初始化,但是指向一个空的数组。

以上两种方式实用性都不高。下面的方法:

var v []int = make([]int, 10, 50)

或者

v := make([]int, 10, 50)

这样分配一个有 50 个 int 值的数组,并且创建了一个长度为 10,容量为 50 的 切片 v,该 切片 指向数组的前 10 个元素。

问题 3 给定 s := make([]byte, 5),len(s) 和 cap(s) 分别是多少?s = s[2:4],len(s) 和 cap(s) 又分别是多少? 问题 4 假设 s1 := []byte{'p', 'o', 'e', 'm'}s2 := s1[2:],s2 的值是多少?如果我们执行 s2[1] = 't',s1 和 s2 现在的值又分别是多少?

2.5 多维 切片

和数组一样,切片通常也是一维的,但是也可以由一维组合成高维。通过分片的分片(或者切片的数组),长度可以任意动态变化,所以 Go 语言的多维切片可以任意切分。而且,内层的切片必须单独分配(通过 make 函数)。

2.6 bytes 包

类型 []byte 的切片十分常见,Go 语言有一个 bytes 包专门用来解决这种类型的操作方法。

bytes 包和字符串包十分类似(参见第 4.7 节)。而且它还包含一个十分有用的类型 Buffer:

import "bytes"

type Buffer struct {
    ...
}

这是一个长度可变的 bytes 的 buffer,提供 Read 和 Write 方法,因为读写长度未知的 bytes 最好使用 buffer。

Buffer 可以这样定义:var buffer bytes.Buffer

或者使用 new 获得一个指针:var r *bytes.Buffer = new(bytes.Buffer)

或者通过函数:func NewBuffer(buf []byte) *Buffer,创建一个 Buffer 对象并且用 buf 初始化好;NewBuffer 最好用在从 buf 读取的时候使用。

通过 buffer 串联字符串

类似于 Java 的 StringBuilder 类。

在下面的代码段中,我们创建一个 buffer,通过 buffer.WriteString(s) 方法将字符串 s 追加到后面,最后再通过 buffer.String() 方法转换为 string:

var buffer bytes.Buffer
for {
    if s, ok := getNextString(); ok { //method getNextString() not shown here
        buffer.WriteString(s)
    } else {
        break
    }
}
fmt.Print(buffer.String(), "n")

这种实现方式比使用 += 要更节省内存和 CPU,尤其是要串联的字符串数目特别多的时候。

3 For-range 结构

这种构建方法可以应用与数组和切片:

for ix, value := range slice1 {
    ...
}

第一个返回值 dx 是数组或者切片的索引,第二个是在该索引位置的值;他们都是仅在 for 循环内部可见的局部变量。value 只是 slice1 某个索引位置的值的一个拷贝,不能用来修改 slice1 该索引位置的值。

示例 9 slices_forrange.go

package main
import "fmt"

func main() {
    var arr1 [5]int

    for i:=0; i < len(arr1); i++ {
        arr1[i] = i * 2
    }

    for i:=0; i < len(arr1); i++ {
        fmt.Printf("Array at index %d is %dn", i, arr1[i])
    }
}

示例 10 slices_forrange2.go

package main
import "fmt"

func main() {
    seasons := []string{"Spring", "Summer", "Autumn", "Winter"}
    for ix, season := range seasons {
        fmt.Printf("Season %d is: %sn", ix, season)
    }

    var season string
    for _, season = range seasons {
        fmt.Printf("%sn", season)
    }
}

slices*forrange2.go 给出了一个关于字符串的例子, * 可以用于忽略索引。

如果你只需要索引,你可以忽略第二个变量,例如:

for ix := range seasons {
    fmt.Printf("%d", ix)
}
// Output: 0 1 2 3

如果你需要修改 seasons[ix] 的值可以使用这个版本。

多维切片下的 for-range:

通过计算行数和矩阵值可以很方便的写出如(参考第 1.3 节)的 for 循环来,例如(参考第 5 节的例子 multidim_array.go):

for row := range screen {
    for column := range screen[0] {
        screen[row][column] = 1
    }
}

问题 5 假设我们有如下数组:items := [...]int{10, 20, 30, 40, 50}

a) 如果我们写了如下的 for 循环,那么执行完 for 循环后的 items 的值是多少?如果你不确定的话可以测试一下:)

for _, item := range items {
    item *= 2
}

b) 如果 a) 无法正常工作,写一个 for 循环让值可以 double。

4 切片重组(reslice)

我们已经知道切片创建的时候通常比相关数组小,例如:

slice1 := make([]type, start_length, capacity)

其中 start_length 作为切片初始长度而 capacity 作为相关数组的长度。

这么做的好处是我们的切片在达到容量上限后可以扩容。改变切片长度的过程称之为切片重组 reslicing,做法如下:slice1 = slice1[0:end],其中 end 是新的末尾索引(即长度)。

将切片扩展 1 位可以这么做:

sl = sl[0:len(sl)+1]

切片可以反复扩展直到占据整个相关数组。

示例 11 reslicing.go

package main
import "fmt"

func main() {
    slice1 := make([]int, 0, 10)
    // load the slice, cap(slice1) is 10:
    for i := 0; i < cap(slice1); i++ {
        slice1 = slice1[0:i+1]
        slice1[i] = i
        fmt.Printf("The length of slice is %dn", len(slice1))
    }

    // print the slice:
    for i := 0; i < len(slice1); i++ {
        fmt.Printf("Slice at %d is %dn", i, slice1[i])
    }
}

输出结果:

The length of slice is 1
The length of slice is 2
The length of slice is 3
The length of slice is 4
The length of slice is 5
The length of slice is 6
The length of slice is 7
The length of slice is 8
The length of slice is 9
The length of slice is 10
Slice at 0 is 0
Slice at 1 is 1
Slice at 2 is 2
Slice at 3 is 3
Slice at 4 is 4
Slice at 5 is 5
Slice at 6 is 6
Slice at 7 is 7
Slice at 8 is 8
Slice at 9 is 9

另一个例子:

var ar = [10]int{0,1,2,3,4,5,6,7,8,9}
var a = ar[5:7] // reference to subarray {5,6} - len(a) is 2 and cap(a) is 5

将 a 重新分片:

a = a[0:4] // ref of subarray {5,6,7,8} - len(a) is now 4 but cap(a) is still 5

5 切片的复制与追加

如果想增加切片的容量,我们必须创建一个新的更大的切片并把原分片的内容都拷贝过来。下面的代码描述了从拷贝切片的 copy 函数和向切片追加新元素的 append 函数。

示例 12 copy_append_slice.go

package main
import "fmt"

func main() {
    sl_from := []int{1, 2, 3}
    sl_to := make([]int, 10)

    n := copy(sl_to, sl_from)
    fmt.Println(sl_to)
    fmt.Printf("Copied %d elementsn", n) // n == 3

    sl3 := []int{1, 2, 3}
    sl3 = append(sl3, 4, 5, 6)
    fmt.Println(sl3)
}

func append(s[]T, x ...T) []T 其中 append 方法将 0 个或多个具有相同类型 s 的元素追加到切片后面并且返回新的切片;追加的元素必须和原切片的元素同类型。如果 s 的容量不足以存储新增元素,append 会分配新的切片来保证已有切片元素和新增元素的存储。因此,返回的切片可能已经指向一个不同的相关数组了。append 方法总是返回成功,除非系统内存耗尽了。

如果你想将切片 y 追加到切片 x 后面,只要将第二个参数扩展成一个列表即可:x = append(x, y...)

注意: append 在大多数情况下很好用,但是如果你想完全掌控整个追加过程,你可以实现一个这样的 AppendByte 方法:

func AppendByte(slice []byte, data ...byte) []byte {
    m := len(slice)
    n := m + len(data)
    if n > cap(slice) { // if necessary, reallocate
        // allocate double what's needed, for future growth.
        newSlice := make([]byte, (n+1)*2)
        copy(newSlice, slice)
        slice = newSlice
    }
    slice = slice[0:n]
    copy(slice[m:n], data)
    return slice
}

func copy(dst, src []T) int copy 方法将类型为 T 的切片从源地址 src 拷贝到目标地址 dst,覆盖 dst 的相关元素,并且返回拷贝的元素个数。源地址和目标地址可能会有重叠。拷贝个数是 src 和 dst 的长度最小值。如果 src 是字符串那么元素类型就是 byte。如果你还想继续使用 src,在拷贝结束后执行 src = dst

6 字符串、数组和切片的应用

6.1 从字符串生成字节切片

假设 s 是一个字符串(本质上是一个字节数组),那么就可以直接通过 c := []bytes(s) 来获取一个字节的切片 c。另外,您还可以通过 copy 函数来达到相同的目的:copy(dst []byte, src string)

同样的,还可以使用 for-range 来获得每个元素(Listing 13—for_string.go):

package main

import "fmt"

func main() {
    s := "u00ffu754c"
    for i, c := range s {
        fmt.Printf("%d:%c ", i, c)
    }
}

输出:

0:ÿ 2:界

我们知道,Unicode 字符会占用 2 个字节,有些甚至需要 3 个或者 4 个字节来进行表示。如果发现错误的 UTF8 字符,则该字符会被设置为 U+FFFD 并且索引向前移动一个字节。和字符串转换一样,您同样可以使用 c := []int(s) 语法,这样切片中的每个 int 都会包含对应的 Unicode 代码,因为字符串中的每次字符都会对应一个整数。类似的,您也可以将字符串转换为元素类型为 rune 的切片:r := []rune(s)

可以通过代码 len([]int(s)) 来获得字符串中字符的数量,但使用 utf8.RuneCountInString(s) 效率会更高一点。(参考count_characters.go)

您还可以将一个字符串追加到某一个字符数组的尾部:

var b []byte
var s string
b = append(b, s...)

6.2 获取字符串的某一部分

使用 substr := str[start:end] 可以从字符串 str 获取到从索引 start 开始到 end-1 位置的子字符串。同样的,str[start:] 则表示获取从 start 开始到 len(str)-1 位置的子字符串。而 str[:end] 表示获取从 0 开始到 end-1 的子字符串。

6.3 字符串和切片的内存结构

在内存中,一个字符串实际上是一个双字结构,即一个指向实际数据的指针和记录字符串长度的整数(见图 4)。因为指针对用户来说是完全不可见,因此我们可以依旧把字符串看做是一个值类型,也就是一个字符数组。

字符串 string s = "hello" 和子字符串 t = s[2:3] 在内存中的结构可以用下图表示:

8fa4364184ae3cca7c9fded7e486e7eb.png

6.4 修改字符串中的某个字符

Go 语言中的字符串是不可变的,也就是说 str[index] 这样的表达式是不可以被放在等号左侧的。如果尝试运行 str[i] = 'D' 会得到错误:cannot assign to str[i]

因此,您必须先将字符串转换成字节数组,然后再通过修改数组中的元素值来达到修改字符串的目的,最后将字节数组转换回字符串格式。

例如,将字符串 "hello" 转换为 "cello":

s := "hello"
c := []byte(s)
c[0] = ’c’
s2 := string(c) // s2 == "cello"

所以,您可以通过操作切片来完成对字符串的操作。

6.5 字节数组对比函数

下面的 Compare 函数会返回两个字节数组字典顺序的整数对比结果,即 0 if a == b, -1 if a < b, 1 if a > b

func Compare(a, b[]byte) int {
    for i:=0; i < len(a) && i < len(b); i++ {
        switch {
        case a[i] > b[i]:
            return 1
        case a[i] < b[i]:
            return -1
        }
    }
    // 数组的长度可能不同
    switch {
    case len(a) < len(b):
        return -1
    case len(a) > len(b):
        return 1
    }
    return 0 // 数组相等
}

6.6 搜索及排序切片和数组

标准库提供了 sort 包来实现常见的搜索和排序操作。您可以使用 sort 包中的函数 func Ints(a []int)来实现对 int 类型的切片排序。例如 sort.Ints(arri),其中变量 arri 就是需要被升序排序的数组或切片。为了检查某个数组是否已经被排序,可以通过函数 IntsAreSorted(a []int) bool 来检查,如果返回 true 则表示已经被排序。

类似的,可以使用函数 func Float64s(a []float64) 来排序 float64 的元素,或使用函数 func Strings(a []string) 排序字符串元素。

想要在数组或切片中搜索一个元素,该数组或切片必须先被排序(因为标准库的搜索算法使用的是二分法)。然后,您就可以使用函数 func SearchInts(a []int, n int) int 进行搜索,并返回对应结果的索引值。

当然,还可以搜索 float64 和字符串:

func SearchFloat64s(a []float64, x float64) int
func SearchStrings(a []string, x string) int

您可以通过查看 官方文档 来获取更详细的信息。

这就是如何使用 sort 包的方法,我们会在第 11.6 节对它的细节进行深入,并实现一个属于我们自己的版本。

6.7 append 函数常见操作

我们在第 5 节提到的 append 非常有用,它能够用于各种方面的操作:

  1. 将切片 b 的元素追加到切片 a 之后:a = append(a, b...)
  2. 复制切片 a 的元素到新的切片 b 上:

go b = make([]T, len(a)) copy(b, a)

  1. 删除位于索引 i 的元素:a = append(a[:i], a[i+1:]...)
  2. 切除切片 a 中从索引 i 至 j 位置的元素:a = append(a[:i], a[j:]...)
  3. 为切片 a 扩展 j 个元素长度:a = append(a, make([]T, j)...)
  4. 在索引 i 的位置插入元素 x:a = append(a[:i], append([]T{x}, a[i:]...)...)
  5. 在索引 i 的位置插入长度为 j 的新切片:a = append(a[:i], append(make([]T, j), a[i:]...)...)
  6. 在索引 i 的位置插入切片 b 的所有元素:a = append(a[:i], append(b, a[i:]...)...)
  7. 取出位于切片 a 最末尾的元素 x:x, a = a[len(a)-1], a[:len(a)-1]
  8. 将元素 x 追加到切片 a:a = append(a, x)

因此,您可以使用切片和 append 操作来表示任意可变长度的序列。

从数学的角度来看,切片相当于向量,如果需要的话可以定义一个向量作为切片的别名来进行操作。

如果您需要更加完整的方案,可以学习一下 Eleanor McHugh 编写的几个包:slices、chain 和 lists。

6.8 切片和垃圾回收

切片的底层指向一个数组,该数组的实际体积可能要大于切片所定义的体积。只有在没有任何切片指向的时候,底层的数组内层才会被释放,这种特性有时会导致程序占用多余的内存。

示例 函数 FindDigits 将一个文件加载到内存,然后搜索其中所有的数字并返回一个切片。

var digitRegexp = regexp.MustCompile("[0-9]+")

func FindDigits(filename string) []byte {
    b, _ := ioutil.ReadFile(filename)
    return digitRegexp.Find(b)
}

这段代码可以顺利运行,但返回的 []byte 指向的底层是整个文件的数据。只要该返回的切片不被释放,垃圾回收器就不能释放整个文件所占用的内存。换句话说,一点点有用的数据却占用了整个文件的内存。

想要避免这个问题,可以通过拷贝我们需要的部分到一个新的切片中:

func FindDigits(filename string) []byte {
   b, _ := ioutil.ReadFile(filename)
   b = digitRegexp.Find(b)
   c := make([]byte, len(b))
   copy(c, b)
   return c
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值