Golang의 배열 조각은 어리석고 불분명합니다.

정렬

Go 개발자들은 일상 업무에서 슬라이스를 많이 사용합니다. 슬라이스를 소개하기 전에 먼저 배열에 대해 알아보겠습니다. 모든 사람이 배열에 익숙할 것이라고 생각합니다. 배열의 데이터 구조는 비교적 간단하고 메모리에서 연속적입니다. 10개의 숫자 배열을 예로 들어 보겠습니다.

a:=[10]int{0,1,2,3,4,5,6,7,8,9}

메모리에 다음과 같이 표시됩니다.

이미지.png연속성 덕분에 어레이의 특성은 다음과 같습니다.

  • 고정 크기
  • 액세스가 빠르고 복잡도는 O(1)입니다.
  • 요소 삽입 및 삭제는 요소 이동으로 인해 쿼리보다 느립니다.

범위를 벗어난 요소의 요소에 액세스하려고 할 때 go는 편집조차 하지 않습니다.

a := [10]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
fmt.Println(a[10])
// invalid array index 10 (out of bounds for 10-element array)

일부분

배열에 비해 go의 슬라이스(슬라이스)는 비교적 유연합니다. 가장 큰 차이점은 슬라이스의 길이를 고정할 수 없다는 점입니다. 생성할 때 길이를 지정할 필요가 없습니다. go에서 슬라이스는 설계된 데이터 구조입니다.

type slice struct {
   array unsafe.Pointer //指针
   len   int //长度
   cap   int //容量
}

슬라이스의 맨 아래 레이어는 실제로 배열입니다. 포인터는 기본 배열을 가리킵니다. len은 슬라이스의 길이이고 cap은 슬라이스의 용량입니다. 슬라이스에 요소를 추가할 때 cap의 용량은 부족할 경우 정책에 따라 용량을 증설합니다.

이미지.png

슬라이스 생성

직접적인 진술

var s []int

직접 선언된 슬라이스를 통해 그것은 nil슬라이스이고, 그 길이와 용량은 0이고, 기본 배열을 가리키지 않으며, nil 슬라이스와 빈 슬라이스는 서로 다르며 다음에 소개합니다.

새로운 메소드 초기화

s:=*new([]int) 

새로운 방법은 직접 선언 방법과 크게 다르지 않으며 최종 출력은 nil 슬라이스입니다.

정확한

s1 := []int{0, 1, 2}
s2 := []int{0, 1, 2, 4: 4}
s3 := []int{0, 1, 2, 4: 4, 5, 6, 9: 9}
fmt.Println(s1, len(s1), cap(s1)) //[0 1 2] 3 3
fmt.Println(s2, len(s2), cap(s2)) //[0 1 2 0 4] 5 5
fmt.Println(s3, len(s3), cap(s3)) //[0 1 2 0 4 5 6 0 0 9] 10 10

리터럴에 의해 생성된 슬라이스의 기본 길이와 용량은 동일하며 인덱스 값을 별도로 지정하면 인덱스 값 앞의 요소를 선언하지 않으면 기본 슬라이스 유형이 된다는 점에 유의해야 합니다. .

만드는 방법

s := make([]int, 5, 6)
fmt.Println(s, len(s), cap(s)) //[0 0 0 0 0] 5 6

슬라이스의 길이와 용량은 make로 지정할 수 있습니다.

차단 방법

슬라이스는 배열 또는 다른 슬라이스에서 얻을 수 있습니다. 이때 새 슬라이스는 이전 어레이 또는 슬라이스와 기본 배열을 공유합니다. 누가 데이터를 수정하든 상관없이 기본 어레이에 영향을 미치지만 새 슬라이스가 확장되면 , 그러면 기본 배열이 동일하지 않습니다.

에스[:]

a := []int{0, 1, 2, 3, 4}
b := a[:]
fmt.Println(b, len(b), cap(b)) //[0 1 2 3 4] 5 5

에서 얻은 슬라이스 : 전체 슬라이스에 대한 참조와 같습니다.[0,len(a)-1]

시:]

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

通过指定切片的开始位置来获取切片,它是左闭的包含左边的元素,此时它的容量cap(b)=cap(a)-i。这里要注意界限问题,a[5:]的话,相当于走到数组的尾巴处,什么元素也没了,此时就是个空切片,但是如果你用a[6:]的话,那么就会报错,超出了数组的界限。

a := []int{0, 1, 2, 3, 4}
b := a[5:] //[]
c := a[6:] //runtime error: slice bounds out of range [6:5]

c虽然报错了,但是它只是运行时报错,编译还是能通过的

s[:j]

a := []int{0, 1, 2, 3, 4}
b := a[:4]
fmt.Println(b, len(b), cap(b)) //[0 1 2 3] 4 5

获取[0-j)的数据,注意右边是开区间,不包含j,同时它的cap和j没关系,始终是cap(b) = cap(a),同样注意不要越界。

s[i:j]

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

获取[i-j)的数据,注意右边是开区间,不包含j,它的cap(b) = cap(a)-i

s[i:j:x]

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

通过上面的例子,我们可以发现切片b的cap其实和j没什么关系,和i存在关联,不管j是什么,始终是cap(b)=cap(a)-ix的出现可以修改b的容量,当我们设置x后,cap(b) = x-i而不再是cap(a)-i了。

看个例子

s0 := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
s1 := s0[3:6] //[3 4 5] 3 7

s1是对s0的切片,所以它们大概是这样:

이미지.png

s2 := s1[1:3:4]

这时指定个s2,s2是对s1的切片,并且s2的len=2,cap=3,所以大概长这样:

이미지.png

s1[1] = 40
fmt.Println(s0, s1, s2)// [0 1 2 3 40 5 6 7 8 9] [3 40 5] [40 5]

这时把s1[1]修改成40,因为没有涉及到扩容,s0、s1、s2重叠部分都指向同一个底层数组,所以最终发现s0、s2对应的位置都变成了40。

이미지.png

s2 = append(s2, 10)
fmt.Println(s2, len(s2), cap(s2)) //[40 5 10] 3 3

再向s2中添加一个元素,因为s2还有一个空间,所以不用发生扩容。

이미지.png

s2 = append(s2, 11)
fmt.Println(s2, len(s2), cap(s2)) //[40 5 10 11] 4 6

继续向s2中添加一个元素,此时s2已经没有空间了,所以会触发扩容,扩容后指向一个新的底层数据,和原来的底层数组解耦了。

이미지.png 此时无论怎么修改s2都不会影响到s1和s2。

切片的扩容

slice的扩容主要通过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 {
            // 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
            }
        }
    }
    ....
    return slice{p, old.len, newcap}
}

入参说明下:

  1. et是slice的类型。
  2. old是老的slice。
  3. cap是扩容后的最低容量,比如原来是4,append加了一个,那么cap就是5。

所以上面的代码解释为:

  1. 如果扩容后的最低容量大于老的slice的容量的2倍,那么新的容量等于扩容后的最低容量。
  2. 如果老的slice的长度小于1024,那么新的容量就是老的slice的容量的2倍
  3. 如果老的slice的长度大于等于1024,那么新的容量就等于的容量不停的1.25倍,直至大于扩容后的最低容量。

这里需要说明下关于slice的扩容网上很多文章都说小于1024翻倍扩容,大于1024每次1.25倍扩容,其实就是基于这段代码,但其实这不全对,我们来看个例子:

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

按照规则1,这时的cap应该是5,结果是6。

a := make([]int, 1280, 1280)
fmt.Println(len(a), cap(a)) //1280 1280
a = append(a, 1)
fmt.Println(len(a), cap(a), 1280*1.25) //1281 1696 1600

按照规则3,这时的cap应该是原来的1.25倍,即1600,结果是1696。

内存对齐

其实上面两个扩容,只能说不是最终的结果,go还会做一些内存对齐的优化,通过内存对齐可以提升读取的效率。

// 内存对齐
capmem, overflow = math.MulUintptr(et.size, uintptr(newcap))
capmem = roundupsize(capmem)
newcap = int(capmem / et.size)

空切片和nil切片

空切片:slice的指针不为空,len和cap都是0
nil切片:slice的指针不指向任何地址即array=0,len和cap都是0

nil
var a []int a:=make([]int,0)
a:=*new([]int) a:=[]int{}

空切片虽然地址不为空,但是这个地址也不代表任何底层数组的地址,空切片在初始化的时候会指向一个叫做zerobase的地址,

var zerobase uintptr
if size == 0 {
      return unsafe.Pointer(&zerobase)
}

所有空切片的地址都是一样的。

var a1 []int
a2:=*new([]int)
a3:=make([]int,0)
a4:=[]int{}

fmt.Println(*(*[3]int)(unsafe.Pointer(&a1))) //[0 0 0]
fmt.Println(*(*[3]int)(unsafe.Pointer(&a2))) //[0 0 0]
fmt.Println(*(*[3]int)(unsafe.Pointer(&a3))) //[824634101440 0 0]
fmt.Println(*(*[3]int)(unsafe.Pointer(&a4))) //[824634101440 0 0]

数组是值传递,切片是引用传递?

func main() {
   array := [10]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
   slice := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
   changeArray(array)
   fmt.Println(array) //[0 1 2 3 4 5 6 7 8 9]
   changeSlice(slice)
   fmt.Println(slice) //[1 1 2 3 4 5 6 7 8 9]
}

func changeArray(a [10]int) {
   a[0] = 1
}

func changeSlice(a []int) {
   a[0] = 1
}
  • 定义一个数组和一个切片
  • 通过changeArray改变数组下标为0的值
  • 通过changeSlice改变切片下标为0的值
  • 原数组值未被修改,原切片的值已经被修改

这个表象看起来像是slice是指针传递似的,但是如果我们这样呢:


func main() {
   slice := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
   changeSlice(slice)//[0 1 2 3 4 5 6 7 8 9]
}
func changeSlice(a []int) {
   a = append(a, 99)
}

원래 슬라이스의 값이 변경되지 않았음을 알 수 있습니다. 이것은 추가를 사용하기 때문입니다. 추가 후 원래 슬라이스의 용량이 충분하지 않습니다. 이때 새 배열이 복사됩니다. 실제로 go의 함수 매개변수는 참조가 아닌 값으로만 ​​전달됩니다. 슬라이스의 기본 데이터가 변경되지 않은 경우 수정하는 방법이 원래 기본 배열에 영향을 줍니다. 슬라이스가 확장되면 새 어레이가 됩니다. 확장 후 이 새 어레이를 수정하는 방법은 원래 어레이에 영향을 주지 않습니다.

배열과 슬라이스를 비교할 수 있습니까?

길이와 유형이 같은 배열만 비교할 수 있습니다.

a:=[2]int{1,2}
b:=[2]int{1,2}
fmt.Println(a==b) true

a:=[2]int{1,2}
b:=[3]int{1,2,3}
fmt.Println(a==b) //invalid operation: a == b (mismatched types [2]int and [3]int)

a:=[2]int{1,2}
b:=[2]int8{1,2}
fmt.Println(a==b) //invalid operation: a == b (mismatched types [2]int and [2]int8)

슬라이스는 nil과만 비교할 수 있고 나머지는 비교할 수 없습니다.

a:=[]int{1,2}
b:=[]int{1,2}
fmt.Println(a==b)//invalid operation: a == b (slice can only be compared to nil)

그러나 둘 다 nil인 두 슬라이스는 비교할 수 없으며 nil과만 비교할 수 있습니다. 여기서 nil은 실제 nil입니다.

var a []int
var b []int
fmt.Println(a == b) //invalid operation: a == b (slice can only be compared to nil)
fmt.Println(a == nil) //true

WeChat 검색 "프로그래밍을 이해하는 척"

Ich denke du magst

Origin juejin.im/post/7121628307040403487
Empfohlen
Rangfolge