golang学习-切片

golang学习-切片

  • Go切片
    • go语言slice的用法
    • go语言slice实例
    • go语言slice的长度和容量
    • go语言slice的初始化
      • 直接初始化
      • 使用数组初始化
      • 使用数组部分元素初始化
    • go语言slice的遍历
      • for遍历
      • for range遍历
    • go语言slice的添加和删除
      • 添加元素
      • 删除元素
      • 拷贝slice
    • slice底层探究
      • slice底层结构
      • 通过截取数组创建的slice
      • 对应内存结构图
      • 通过make方式创建的slice

Go切片

前面我们学习了数组,数组是固定长度,可以容纳相同数据类型的元素的集合。当长度固定时,使用还是带来了一些限制,比如:申请的长度太大浪费内存,太小又不够用。

鉴于上述原因,就有了go语言的切片(slice),可以把slice理解为,可变长度的数组,其实它底层就是使用数组实现的,增加了自动扩容功能。Slice是一个拥有相同类型元素的可变长度的序列。

go语言slice的用法

声明一个slice和声明一个数组类似,只要不添加长度就可以了

var identifier []type

slice是引用类型,可以使用make函数来创建slice:

var slice1 []type = make([]type , len)

也可以简写为

slice1 := make([]type , len)

也可以指定容量,其中capacity为可选参数。

make([]T , length , capacity)

这里len是数组的长度也是slice的初始长度。

go语言slice实例

package main
	
import(
	"fmt"
)
	
func main(){
	var slice1 []int
	var slice2 []string

	fmt.Printf("%T\n",slice1) //[]int
	fmt.Printf("%T\n",slice2) //[]string
	fmt.Println(slice1 == nil) //true
	fmt.Println(slice2 == nil) //true
    
    slice1 := make([]int, 2)
	fmt.Printf("slice1: %v\n", slice1)
}

go语言slice的长度和容量

slice拥有自己的长度和容量,我们可以通过内置的len()函数求长度,使用内置的cap()函数求slice的容量。

实例

package main

import (
	"fmt"
)

func test3() {
	var name = []string{"ljy", "23"}
	var numbers = []int{1, 2, 3}

	fmt.Printf("len:%d cap:%d\n", len(name), cap(name))
	fmt.Printf("len:%d cap:%d\n", len(numbers), cap(numbers))

	s1 := make([]string, 2, 3)
	fmt.Printf("len:%d cap:%d\n", len(s1), cap(s1))
}

go语言slice的初始化

slice的初始化方法很多,可以直接初始化,也可以使用数组初始化等。

直接初始化

package main

import (
	"fmt"
)

// 直接初始化slice
func main() {
	s1 := []int{1, 2, 3}
	fmt.Printf("s1: %v\n", s1) //s1: [1 2 3]
}

使用数组初始化

package main

import (
	"fmt"
)

// 使用数组初始化slice
func main() {
	arr := [...]int{1, 2, 3}
	s1 := arr[:] //取数组所有元素
	fmt.Printf("s1: %v\n", s1) //s1: [1 2 3]
	fmt.Printf("s1: %T\n", s1) //s1: []int
}

使用数组部分元素初始化

slice的底层就是一个数组,所以我们可以基于数组通过slice表达式得到slice。slice表达式中的low和high表示一个索引范围(左包含,右不包含),得到的slice长度=high-low,容量等于得到的slice的底层数组的容量。

package main

import (
	"fmt"
)

// 使用数组部分元素初始化
func main() {
	arr := [...]int{1, 2, 3, 4, 5, 6}
	s1 := arr[2:5]
	fmt.Printf("s1: %v\n", s1) //s1: [3 4 5]
	s2 := arr[2:]
	fmt.Printf("s2: %v\n", s2) //s2: [3 4 5 6]
	s3 := arr[:3]
	fmt.Printf("s3: %v\n", s3) //s3: [1 2 3]
}

go语言slice的遍历

for遍历

package main

import (
	"fmt"
)

// for循环遍历
func main() {
	var s = []int{1, 2, 3, 4, 5}

	for i := 0; i < len(s); i++ {
		fmt.Printf("s[i]: %v\t", s[i])
	}
}

for range遍历

package main

import (
	"fmt"
)

// for range遍历
func main() {
	var s = []int{1, 2, 3, 4, 5}

	for _, v := range s {
		fmt.Printf("v: %v\t", v)
	}
}

go语言slice的添加和删除

slice是一个动态数组,可以使用append()函数添加元素,go语言中没有删除slice元素的专用方法,我们可以使用slice本身的特性来删除元素。由于,slice是引用类型,通过赋值的方式,会修改原有内容,go提供了copy()函数来拷贝slice。

添加元素

package main

import (
	"fmt"
)

// 添加元素
func main() {
	s := []int{}
	s = append(s, 1)
	s = append(s, 2)       // 添加单个元素
	s = append(s, 3, 4, 5) // 添加多个元素
	fmt.Printf("s: %v\n", s)

	s1 := []int{3, 4, 5}
	s2 := []int{6, 7}
	s1 = append(s1, s2...) // 添加另外一个slice
	fmt.Printf("s1: %v\n", s1)
}

删除元素

package main

import (
	"fmt"
)

// 删除元素
func main() {
	s := []int{1, 2, 3, 4, 5}
	fmt.Printf("s: %v\n", s)
	s = append(s[:2], s[3:]...)
	fmt.Printf("s: %v\n", s)
}

公式:要从slicea中删除索引为index的元素,操作方法是a = append(a[:index],a[index+1:]...)

拷贝slice

package main

import (
	"fmt"
)

// 拷贝slice
func test11() {
	s := []int{1, 2, 3}

	s1 := make([]int, 3)
	fmt.Printf("s1: %v\n", s1)
	copy(s1, s)
	fmt.Printf("s1: %v\n", s1)
}

slice底层探究

那么我们首先就要从slice的底层结构看起

slice底层结构

type slice struct {
    array unsafe.Pointer //数组首地址指针
    len   int //长度
    cap   int //容量
}

我们从slice底层结构可以看出,slice结构体有三个属性,分别是unsafe.Pointerlencap,他们分别对应slice对应数组的指针、slice的长度、slice的容量。在得知slice底层结构后,我们来看第一个问题。

通过截取数组创建的slice

package main

import (
	"fmt"
)

// slice在内存中的布局
func test12() {
	arr := [...]int{1, 2, 3, 4, 5}
	slice := arr[2:3]

	fmt.Printf("arr[2]: %p\n", &arr[2])     //arr[2]: 0xc00000c460
	fmt.Printf("slice[0]: %p\n", &slice[0]) //slice[0]: 0xc00000c460
	// 这也就表明了实际上slice是在操作原有数组
	// 下面我们修改slice,然后观察数组的值,从而进行判断
	slice[0] = 10                          //10
	fmt.Printf("slice[0]: %v\n", slice[0]) //10
	fmt.Printf("arr[2]: %v\n", arr[2])     //10
	// 以上便是证明slice操作原数组的验证

	// 那么slice一直都在操作原数组么?什么时候会有变化?
	// 我们可以先查看slice的unsafe.Pointer、cap和len
	fmt.Printf("slice: %p %v %v\n", &slice[0], len(slice), cap(slice)) // slice: 0xc00000c460 2 3
	// 下面我们尝试向slice中添加元素
	slice = append(slice, 91)
	// 可以看出只有len有变化,是因为向slice中添加了元素
	fmt.Printf("slice: %p %v %v\n", &slice[0], len(slice), cap(slice)) // slice: 0xc00000c460 3 3
	// 继续尝试添加元素
	slice = append(slice, 92)
	// 此时我们可以看到,slice的unsafe.Pointer并不再指向截取arr的地址,而是一个新的地址
	fmt.Printf("slice: %p %v %v\n", &slice[0], len(slice), cap(slice)) // slice: 0xc00000c480 4 6

	// 上述尝试中,我们在修改slice中的元素后,会对应修改arr中的元素,那么我们添加元素是否会影响arr呢?
	for _, v := range arr {
		fmt.Printf("v: %v\t", v) // v: 1	v: 2	v: 10	v: 91	v: 92	
	}
	// 此时我们可以看到,追加元素同样会影响arr,追加元素在slice不用扩容的情况下,会默认覆盖arr对应的元素
}

对应内存结构图

golang学习-切片_第1张图片

通过make方式创建的slice

package main

import (
	"fmt"
)

func test13() {
	slice := make([]int, 0, 3)
	// slice: [] slice address: 0xc000004078 len: 0 cap: 3
	fmt.Printf("slice: %v slice address: %p len: %v cap: %v\n", slice, &slice, len(slice), cap(slice))

	slice = append(slice, 1)
	// slice: [1] slice Pointer: 0xc00000a120 slice address: 0xc000004078 len: 1 cap: 3
	fmt.Printf("slice: %v slice Pointer: %p slice address: %p len: %v cap: %v\n", slice, &slice[0], &slice, len(slice), cap(slice))
	slice = append(slice, 2)
	slice = append(slice, 3)
	slice = append(slice, 4)
	// slice: [1 2 3 4] slice Pointer: 0xc00000c450 slice address: 0xc000004078 len: 4 cap: 6
	fmt.Printf("slice: %v slice Pointer: %p slice address: %p len: %v cap: %v\n", slice, &slice[0], &slice, len(slice), cap(slice))

}

分别观察两种创建slice方式操作,他们都有一个共同点,那就是在slice进行扩容操作后,unsafe.Poniter地址会发生改变,那么这是为什么?

我们观察runtime.growslice()方法的源码

func growslice(et *_type, old slice, cap int) slice {
  ...
  ...
  if cap < old.cap {
    panic(errorString("growslice: cap out of range"))
  }
  if et.size == 0 {
    // append should not create a slice with nil pointer but non-zero len.
    // We assume that append doesn't need to preserve old.array in this case.
    return slice{unsafe.Pointer(&zerobase), old.len, cap}
  }
  newcap := old.cap// 1
  doublecap := newcap + newcap// 1+1 = 2 为什么不直接*2, 而是使用加法?
  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 // 1.25倍
      }
      // Set newcap to the requested cap when
      // the newcap calculation overflowed.
      if newcap <= 0 {
        newcap = cap
      }
    }
  }
    ...
    switch {
    case et.size == 1:
        lenmem = uintptr(old.len)
        newlenmem = uintptr(cap)
        capmem = roundupsize(uintptr(newcap))
        overflow = uintptr(newcap) > maxAlloc
        newcap = int(capmem)
    case et.size == sys.PtrSize:
        lenmem = uintptr(old.len) * sys.PtrSize
        newlenmem = uintptr(cap) * sys.PtrSize
        capmem = roundupsize(uintptr(newcap) * sys.PtrSize)
        overflow = uintptr(newcap) > maxAlloc/sys.PtrSize
        newcap = int(capmem / sys.PtrSize)
    case isPowerOfTwo(et.size):
        var shift uintptr
        if sys.PtrSize == 8 {
            // Mask shift for better code generation.
            shift = uintptr(sys.Ctz64(uint64(et.size))) & 63
        } else {
            shift = uintptr(sys.Ctz32(uint32(et.size))) & 31
        }
        lenmem = uintptr(old.len) << shift
        newlenmem = uintptr(cap) << shift
        capmem = roundupsize(uintptr(newcap) << shift)
        overflow = uintptr(newcap) > (maxAlloc >> shift)
        newcap = int(capmem >> shift)
    default:
        lenmem = uintptr(old.len) * et.size
        newlenmem = uintptr(cap) * et.size
        capmem, overflow = math.MulUintptr(et.size, uintptr(newcap))
        capmem = roundupsize(capmem)
        newcap = int(capmem / et.size)
    }
     ...
    memmove(p, old.array, lenmem)
}
 
// 
func roundupsize(size uintptr) uintptr {
    if size < _MaxSmallSize { // 32768
        if size <= smallSizeMax-8 {
            return uintptr(class_to_size[size_to_class8[divRoundUp(size, smallSizeDiv)]]) // 申请的内存块个数
        } else {
            return uintptr(class_to_size[size_to_class128[divRoundUp(size-smallSizeMax, largeSizeDiv)]]) 申请的内存块个数
        }
    }
    if size+_PageSize < size {
        return size
    }
    return alignUp(size, _PageSize)
}
 
// alignUp rounds n up to a multiple of a. a must be a power of 2.
func alignUp(n, a uintptr) uintptr {
    return (n + a - 1) &^ (a - 1)
}
 
const _MaxSmallSize   = 32768
const  smallSizeDiv    = 8
const  smallSizeMax    = 1024
const largeSizeDiv    = 128

可以看出以下两点:

  1. 当slice进行扩容时,如果cap<1024,新slice的cap变为原来的两倍;如果cap>102,新slice变为原来的1.25倍。
  2. roundupsize时内存对齐的过程,golnag中内存分配是根据对象大小来分配不同的mspan,为了避免有过多的内存碎片,因此slice在扩容的过程中需要对扩容后的cap容量进行内存对齐的操作,这也就解释了为什么slice在扩容后unsafe.Poniter会发生改变。

总结:

  1. slice的确是一个引用类型
  2. slice从底层来说,其实就是一个数据结构(struce结构体)
  3. slice在扩容后,unsafe.Pointer会发生改变,原因是为了避免有过多的内存碎片。

你可能感兴趣的:(Go,golang,学习,开发语言)