Go切片

2020/02/22 Go 知识点

切片

数组的大小是固定的,在使用时难免会遇到扩容的情况。切片作为数组一个连续片段的引用,它的大小动态可变,我们可以简单将切片理解为动态数组。

img

切片中持有指向底层存储数据数组的指针,长度指当前切片中存储数据的长度,容量指当前切片的容量,即当前切片从它的第一个数据到其对应数组末尾的长度,可以简单理解为切片在其对应数组中可使用的长度。

声明切片

var name []T

  • T 代表切片类型对应的元素类型

切片默认指向一段连续内存区域,可以是数组,也可以是数组本身

从连续区域生成切片是常规的操作,格式:

slice [开始位置:结束位置]
var a = [3]int{1,2,3}
b := a[1,2]

从数据或切片生成的新的切片具有以下特性:

  • 取出的元素数量为:结束位置-起始位置;
  • 取出元素不包括结束位置对应的索引,切片最后一个元素你用slice[len(slice)]获取;
  • 当缺省开始位置时,表示从连续区域开头到结束位置;
  • 当缺生结束位置时,表示从开始位置到整个连续区域末尾;
  • 两者同时缺省时,与切片本身等效;
  • 两者同时为0,等效于空切片,一般用于切片复位;
  • 超界会报错

重置切片,清空拥有的元素

a :=[]int{1,2,3}
fmt.Println(a[0:0)
//[]

make()函数制造切片

make([]T,len,cap)
  • len : 长度
  • cap : 容量

  • 切片不一定必须经过make() 函数才能使用。生成切片、声明后使用append()函数均可以正常使用切片。

append动态添加元素

每个切片会指向一片内存空间,这片空间容纳容量的元素,超过容量,切片就会”扩容”。”扩容”操作往往发生在append调用时。

var car []string
car = append(car,"Old Driver")

//添加多个元素
car = append(car,"ice","Monk")

//添加切片
team := []string{"Pig","Flyingcake","Chicken"}
car = append(car,team...)

fmt.PrintLn(car)

切片的增长规律,参考:https://www.jianshu.com/p/54be5b08a21c

简单的理解如下:

a. 当需要的容量超过原切片容量的两倍时,会使用需要的容量作为新容量。

b. 当原切片长度小于1024时,新切片的容量会直接翻倍。而当原切片的容量大于等于1024时,会反复地增加25%,直到新容量超过所需要的容量。

切片底层是数组逻辑的实现,切片在扩充容量时,会产生一个新数组

为了避免因为切片是否发生扩容的问题导致bug,最好的处理办法还是在必要时使用 copy 来复制数据,保证得到一个新的切片,以避免后续操作带来预料之外的副作用

复制切片元素到另一个切片

copy()函数,可以迅速的讲一个切片的数据复制到另一个切片空间中。

copy( dest Slice, src Slice []T ) int

copy 的返回值表示实际发生复制的元素个数。

package main

import "fmt"

func main()  {

	//引用切片数据
	ref_Data := src_Data

	//预分配足够多的元素切片
	copy_data := make([]int,element_Count)
	//将数据赋值到新的切片空间中
	copy(copy_data,src_Data)

	//修改原始数据的第一个元素
	src_Data[0] = 999

	//打印引用切片的第一个元素
	fmt.Println(ref_Data[0])

	//打印复制切牌呢的第一个和最后一个元素
	fmt.Println(copy_data[0],copy_data[element_Count-1])

	// 复制原始数据从4到6(不包含)
    copy(copy_data,src_Data[4:6])

	for i :=0; i < 5; i++ {
		fmt.Printf("%d ",copy_data[i])
	}
}


/*
  999
  0 999
  4 5 2 3 4
  */

复制空间 独立空间。

从切片中删除元素

GO并没有对删除切片元素提供的专门语法,需要使用切片本身的特性来删除元素。

package main

import "fmt"

func main()  {

    seq := []string{"a", "b", "c", "d", "e"}
    //指定删除位置
    index := 2

	//查看删除位置之前的元素和之后的元素
	fmt.Println(seq[:index], seq[index+1:])

	//将删除点前后的元素连接起来
	seq = append(seq[:index], seq[index+1:]...)

	fmt.Println(seq)
}	

Go中删除元素的本质:是以删除元素为分界点,将前后两个部分的内存重新连接起来。

Array类型的值作为函数参数

作为参数传进函数时,传递的是数组的原始值拷贝,此时在函数内部是无法更新该数组的:

// 数组使用值拷贝传参
func main() {
	x := [3]int{1,2,3}

	func(arr [3]int) {
		arr[0] = 7
		fmt.Println(arr)	// [7 2 3]
	}(x)
	fmt.Println(x)			// [1 2 3]	// 并不是你以为的 [7 2 3]
}

如果想修改参数数组:

  • 直接传递指向这个数组的指针类型:
    // 传址会修改原数据
    func main() {
      x := [3]int{1,2,3}
    
      func(arr *[3]int) {
          (*arr)[0] = 7	
          fmt.Println(arr)	// &[7 2 3]
      }(&x)
      fmt.Println(x)	// [7 2 3]
    }
    
  • 直接使用 slice:即使函数内部得到的是 slice 的值拷贝,但依旧会更新 slice 的原始数据(底层 array)
    // 会修改 slice 的底层 array,从而修改 slice
    func main() {
      x := []int{1, 2, 3}
      func(arr []int) {
          arr[0] = 7
          fmt.Println(x)	// [7 2 3]
      }(x)
      fmt.Println(x)	// [7 2 3]
    }
    

capacity增长

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. 先将旧的slice容量乘以2,如果乘以2后的容量仍小于新的slice容量,则取新的slice容量(append多个elems)
  2. 如果新slice小于等于旧slice容量的2倍,则取旧slice容量乘以2
  3. 如果旧的slice容量大于1024,则新slice容量取旧slice容量乘以1.25

Qusetion

  1. 扩容前后的 Slice 是否相同? 情况一: 原数组还有容量可以扩容(实际容量没有填充完),这种情况下,扩容以后的 数组还是指向原来的数组,对一个切片的操作可能影响多个指针指向相同地址 的 Slice。

情况二: 原来数组的容量已经达到了最大值,再想扩容, Go 默认会先开一片内存区 域,把原来的值拷贝过来,然后再执行 append() 操作。这种情况丝毫不影响 原数组。

要复制一个 Slice,最好使用 Copy 函数。

Search

    Table of Contents