Golangの配列スライスはばかげて不明瞭です

配列

Go開発者は、日常業務でスライスを頻繁に使用します。スライスを紹介する前に、まず配列について理解しましょう。誰もが配列に精通していると思います。配列のデータ構造は比較的単純で、メモリ内で連続しています。例として10個の数字の配列を取り上げます。

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

メモリ内では次のようになります。

image.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の容量は不十分な場合は、ポリシーに従って容量が拡張されます。

image.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

リテラルによって作成されるスライスのデフォルトの長さと容量は同じです。インデックスの値を個別に指定する場合、インデックス値の前の要素が宣言されていない場合、それがデフォルトのタイプのスライスになることに注意してください。 。

makeメソッド

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

スライスの長さと容量はmakeで指定できます。

傍受方法

スライスは、配列または他のスライスから取得できます。この時点で、新しいスライスは、基になる配列を古い配列またはスライスと共有します。データを変更するユーザーに関係なく、基になる配列に影響しますが、新しいスライスが展開されると、その場合、基になる配列は同じではありません。

s [:]

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]

s [i:]

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的切片,所以它们大概是这样:

image.png

s2 := s1[1:3:4]

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

image.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。

image.png

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

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

image.png

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

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

image.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)
}

元のスライスの値が変更されていないことがわかります。これは、appendを使用しているためです。append後、元のスライスの容量が不足しています。このとき、新しい配列がコピーされます。実際、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である2つのスライスは比較できず、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検索「プログラミングを理解しているふりをする」

おすすめ

転載: juejin.im/post/7121628307040403487