目 录CONTENT

文章目录

Go学习系列09-切片

cplinux98
2022-11-08 / 0 评论 / 0 点赞 / 23 阅读 / 1,605 字 / 正在检测是否收录...

什么是切片

切片是相同类型元素的可变长度的集合,通常表示为[]type

同一个切片中的元素类型都是相同的,它看上取很像数组,但没有长度限制

切片的组成

  • 指针
  • 长度(length/len) 切片元素的个数,不能超过切片的容量
  • 容量(capacity/cap) 底层数组的长度

对于这个指针,每个切片的指针都是指向一个“底层数组”,指向数组的第一个从切片访问的元素,这个元素并不一定是数组的第一个元素,一个底层数组可以对应多个切片,这些个切片可以引用数组的任何位置,并且彼此之间的元素可以重叠。

切片的初始值是nil,没有对应的底层数组,长度和容量都为0,检查切片是否为空的方法可以使用len==0来判断

创建切片

  • // 基于数组创建
    var foods = [5]string{
        "a", "b", "c", "d", "e"
    }
    var foodSlice []string = foods[0:3]
    // 数组[开始:结束] 这样就可以基于数组生成一个切片,这个是左闭右开的区间[开始,结束)
    // 在开始索引为0时,可以简写为[:结束]
    // 在结束索引为-1时,可以简写为[开始:]
    
  • // 直接创建
    var foods = []string{"a", "b", "c", "d"}
    // 与数组的区别是,[]里面没有设置长度
    
  • // 使用内置函数make创建切片
    make([]type, len)
    make([]type, len, cap)
    
    foods := make([]string, 6) // 不写cap时,cap=len
    foods := make([]string, 6, 8) // cap 需要>= len
    

引用数组问题

这里的切片都是在底层引用一个或多个数组,在对切片进行切片时,很容易被表面现象迷惑

package main

import "fmt"

func main() {
	var foodsSlice2 = []string{"a", "b", "c", "d", "e"}
	fmt.Println(len(foodsSlice2), cap(foodsSlice2))
	foodsSlice5 := foodsSlice2[1:4]
	fmt.Println(len(foodsSlice5), cap(foodsSlice5))
}
//5 5
//3 4

这里看着切片5是对切片2进行切片3个元素,以为len和cap都是3,但实际上切片5只是将底层数组的引用指针,指向了切片2引用的底层数组里面的索引1处,因为还是一个底层数组,所以cap的容量是从索引1处到底层数组的结尾。

image-20221108154708480

切片的扩容

切片是有len和cap,当len>cap时,切片底层的数组就需要进行扩容,底层会根据旧cap的容量来计算新底层数组的长度,创建了新数组后,会把旧数组里面的内容填充到新数组内。

func sliceCap() {
	s := make([]int, 0, 1)
	c := cap(s)
	for i := 0; i < 2000; i++ {
		s = append(s, i)
		if n := cap(s); n > c {
			fmt.Printf("cap: %d -> %d \n", c, n)
			c = n
		}
	}
}

func main() {
	sliceCap()
	//cap: 1 -> 2
	//cap: 2 -> 4
	//cap: 4 -> 8
	//cap: 8 -> 16
	//cap: 16 -> 32
	//cap: 32 -> 64
	//cap: 64 -> 128
	//cap: 128 -> 256
	//cap: 256 -> 512
	//cap: 512 -> 848
	//cap: 848 -> 1280
	//cap: 1280 -> 1792
	//cap: 1792 -> 2560
}

当切片的容量不足时,我们会调用 runtime.growslice 函数为切片扩容,扩容是为切片分配新的内存空间并拷贝原切片中元素的过程,我们先来看新切片的容量是如何确定的:

func growslice(et *_type, old slice, cap int) slice {
	newcap := old.cap
	doublecap := newcap + newcap
	if cap > doublecap {
		newcap = cap
	} else {
		if old.len < 1024 {
			newcap = doublecap
		} else {
			for 0 < newcap && newcap < cap {
				newcap += newcap / 4
			}
			if newcap <= 0 {
				newcap = cap
			}
		}
	}

在分配内存空间之前需要先确定新的切片容量,运行时根据切片的当前容量选择不同的策略进行扩容:

  1. 如果期望容量大于当前容量的两倍就会使用期望容量;
  2. 如果当前切片的长度小于 1024 就会将容量翻倍;
  3. 如果当前切片的长度大于 1024 就会每次增加 25% 的容量,直到新容量大于期望容量;

以上内容来自 go设计与实现

切片的遍历

切片的遍历和数组是一样的,使用for range,返回的是索引和元素内容

foods := make([]string, 4)
for index,item := range foods{
    fmt.Println(index, item)
}

注意这里遍历时item是foods里面元素的copy,不是foods的元素本身,如果需要通过遍历去修改切片里面的元素,需要使用索引访问foods[0]

复制切片

  • // 从已有切片复制
    var foods = []string{"a", "b", "c"}
    var copyFoods = foods[:]
    fmt.Println(copy_foods)
    
  • // 使用内置函数copy
    func main() {
    	var copyFoods = []string{"e"}
    	var foods = []string{"a", "b", "c"}
    	//copy(copyFoods, foods) // dst ["a"] <= src ["a","b","c"]
    	//fmt.Println(foods, copyFoods)
    
    	copy(foods, copyFoods) // dst ["e"] => src ["e","b","c"]
    	fmt.Println(foods, copyFoods)
    }
    // copy是按照传参dst是目标接收切片,src是源切片
    // 当dst或src是空切片时,不会操作
    // 
    

image-20221108164714003

切片的增删改查

增加元素

可以使用append函数往切片中添加新的元素,append会返回一个新的切片

var foods = []string{"a", "b"}
foods = append(foods, "c")

删除元素

go语言中没有单独的删除切片元素的函数,我们可以曲线删除:

  1. 找到要删除的元素索引
  2. 基于切片创建两个新的切片,一个是要删除索引前面的,一个是要删除索引后面的
  3. 最后使用append将两个切片合并
var foods = []string{"a", "b", "c", "d"}
foods1 = foods[:2]
foods2 = foods[3:]
foods = append(foods1, foods2...)  // 这里要使用...将foods2解构

修改元素

修改就是把底层列表中对应索引中的内存指向修改成新的元素的内存指向

访问对应索引位置的元素,然后将其覆盖

var foods = []string{"a", "b"}
foods[1] = "c"

查询元素

查询也没有类似python中的index函数,需要手动去遍历查询

var foods = []string{"a", "b"}
for index,item := range foods{
    if item == "b"{
        fmt.Printf("元素%s所在索引为%d", item, index)
    }
}
0

评论区