0%

Go Lang slice 学习笔记

切片的数据结构

切片,本身不存储实际数据。切片的数据结构,也被称为slice header,包含指向首元素的指针、切片长度、切片容量:

img

当将切片作为实参时,只会传递slice header。这个传递是值传递,这意味着:

  • 假如在函数里面对slice的元素作赋值操作,由于有指针指着元素,所以原有slice指向的元素也会被改变。
  • 假如在函数里面reslice,并不会影响原slice。

假如想要改变原有slice,有两种写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 第一种,将返回值赋值给原slice,这种方式也就是append的实现
func SubtractOneFromLength(slice []byte) []byte {
slice = slice[0 : len(slice)-1]
return slice
}
func main() {
newSlice := SubtractOneFromLength(slice)
}

// 第二种,用slice指针
func PtrSubtractOneFromLength(slicePtr *[]byte) {
slice := *slicePtr
*slicePtr = slice[0 : len(slice)-1]
}

这引发了另一个需要注意的点,对于slice的方法,假如需要reslice,要用slice指针:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type path []byte

func (p *path) TruncateAtFinalSlash() {
i := bytes.LastIndex(*p, []byte("/"))
if i >= 0 {
*p = (*p)[0:i]
}
}

func main() {
pathName := path("/usr/bin/tso") // Conversion from string to path.
pathName.TruncateAtFinalSlash()
fmt.Printf("%s\n", pathName)
}

切片,可以改变指向的范围,这是成本很低的操作:

1
2
3
4
5
6
7
8
9
func main() {
r := []bool{true, true, true, false, false, false}
fmt.Println(r)
r = r[1:2]
fmt.Println(r)
r = r[0:3]
fmt.Println(r)
# 输出[true true false],即最开始的[1,4]
}

切片,可以切片,即s := s[[begin]:[end]],其中begin和end可以省略。当end超过实际cap时,会panic。

cap() 查看容量,即从下界到数组最后一个元素的个数

len()查看长度,即从下界到上界的个数

切片的零值为nil,此时caplen都为0

1
var a []int  // nil

切片的append

可使用append函数向切片添加元素,对nil切片进行append是正确的。

当append超过切片容量时,会将容量翻倍,且利用内置函数copy进行数据的拷贝,该函数在切片重叠的情况下也可以保证正确拷贝。注意只有被append的slice的指针会指向这块新的区域,基于这个slice的其他slice并不会更新指针。

1
2
3
4
5
func main() {
var s []int
s = append(s, 2, 3, 4)
fmt.Println(s)
}

还可以使用append函数向切片中添加切片:

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

切片的内存管理

注意只要有一个slice使用着底层slice,即使这个上层slice只包含了很少的元素,底层slice的内存也不会被释放掉。这时候假如为了释放不必要的内存占用,需要将上层slice指向的数据拷贝到新的区域:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var digitRegexp = regexp.MustCompile("[0-9]+")

// 整个文件的字节都被加载进去了,且不释放
func FindDigits(filename string) []byte {
b, _ := ioutil.ReadFile(filename)
return digitRegexp.Find(b)
}

// 拷贝到c,释放多余的占用
func CopyDigits(filename string) []byte {
b, _ := ioutil.ReadFile(filename)
b = digitRegexp.Find(b)
c := make([]byte, len(b))
copy(c, b)
return c
}

切片的创建

使用make来创建一维切片

1
2
a := make([]int, 5)  // len(a) == cap(a) == 5
b := make([]int, 0, 5) // len(b) == 0, cap(b) == 5

使用make创建二维切片

第一种方式,该方式允许第二维大小有所变化:

1
2
3
4
5
// 创建一个位数为[dx][dy]的切片
a := make([][]uint8, dx)
for i := range a {
a[i] = make([]uint8, dy)
}

第二种方式,不允许第二维大小有所变化,因为改变了就会有可能覆盖:

1
2
3
4
5
6
7
8
// Allocate the top-level slice, the same as before.
picture := make([][]uint8, YSize) // One row per unit of y.
// Allocate one large slice to hold all the pixels.
pixels := make([]uint8, XSize*YSize) // Has type []uint8 even though picture is [][]uint8.
// Loop over the rows, slicing each row from the front of the remaining pixels slice.
for i := range picture {
picture[i], pixels = pixels[:XSize], pixels[XSize:]
}

切片的遍历

可使用range遍历切片,每次循环会有两个值,一个是元素的下标,一个是元素的值。可使用_忽略其中一个。

1
2
3
4
5
6
7
8
9
10
11
func main() {
pow := []int{4, 1, 5}
for _, value := range pow {
fmt.Printf("%d\n", value)
}

// 只有一个值只会得到下标
for idx := range pow {
fmt.Printf("%d\n", pow[idx])
}
}

切片的技巧

优雅地往slice插入一个value:

1
2
3
4
5
6
7
8
9
10
11
12
13
// Insert inserts the value into the slice at the specified index,
// which must be in range.
// The slice must have room for the new element.
func Insert(slice []int, index, value int) []int {
// Grow the slice by one element.
slice = slice[0 : len(slice)+1]
// Use copy to move the upper part of the slice out of the way and open a hole.
copy(slice[index+1:], slice[index:])
// Store the new value.
slice[index] = value
// Return the result.
return slice
}

优雅地合并两个切片:

1
2
3
4
a := []string{"John", "Paul"}
b := []string{"George", "Ringo", "Pete"}
a = append(a, b...) // equivalent to "append(a, b[0], b[1], b[2])"
// a == []string{"John", "Paul", "George", "Ringo", "Pete"}

优雅地copy:

1
2
3
4
b = make([]T, len(a))
copy(b, a)
// or
b = append([]T(nil), a...)

cut,优雅地删除掉slice中的某一个区间:

1
2
3
4
5
6
7
8
9
// 普通版,对于元素为指针或者结构中含有指针,会存在内存泄漏,因为被删掉的元素仍属于slice
a = append(a[:i], a[j:]...)

// 升级版,将指针设为nil,减少了内存泄漏,但是还是有元素属于slice
copy(a[i:], a[j:])
for k, n := len(a)-j+i, len(a); k < n; k++ {
a[k] = nil // or the zero value of T
}
a = a[:len(a)-j+i]

filter:

1
2
3
4
5
6
7
8
9
10
func filter(a []int) []int {
n := 0
for _, x := range a {
if keep(x) {
a[n] = x
n++
}
}
a = a[:n]
}

reverse:

1
2
3
4
for i := len(a)/2-1; i >= 0; i-- {
opp := len(a)-1-i
a[i], a[opp] = a[opp], a[i]
}

shuffling:

1
2
3
4
5
6
7
for i := len(a) - 1; i > 0; i-- {
j := rand.Intn(i + 1)
a[i], a[j] = a[j], a[i]
}

// 或者直接用math/rand.Shuffle
// 使用了Fisher–Yates算法