Los segmentos de matriz de Golang son tontos y poco claros

formación

Los desarrolladores de Go usan mucho los segmentos en su trabajo diario. Antes de presentar los segmentos, primero comprendamos los arreglos. Creo que todos están familiarizados con los arreglos. La estructura de datos de los arreglos es relativamente simple y es continua en la memoria. Tome una matriz de 10 números como ejemplo:

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

Se ve así en la memoria:

imagen.pngGracias a la continuidad, las características de la matriz son:

  • tamaño fijo
  • El acceso es rápido y la complejidad es O(1);
  • Insertar y eliminar elementos es más lento que consultar debido a los elementos en movimiento.

Cuando queremos acceder a un elemento de un elemento fuera de los límites, go ni siquiera edita:

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)

rodaja

En comparación con los arreglos, los segmentos (segmentos) de go son relativamente flexibles. La gran diferencia es que la longitud de los segmentos no se puede fijar. No es necesario especificar la longitud al crearlos. En go, los segmentos son una estructura de datos diseñada:

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

La capa inferior de una porción es en realidad una matriz. El puntero apunta a la matriz subyacente. len es la longitud de la porción y cap es la capacidad de la porción. Al agregar elementos a la porción, y la capacidad de la tapa es insuficiente, la capacidad se ampliará de acuerdo con la política.

imagen.png

creación de rebanadas

declaración directa

var s []int

A través del segmento declarado directamente, es un nilsegmento, su longitud y capacidad son 0, y no apunta a ninguna matriz subyacente, el segmento nulo y el segmento vacío son diferentes, que se presentarán a continuación.

inicialización del nuevo método

s:=*new([]int) 

El nuevo método no es muy diferente del método de declaración directa, y el resultado final es una porción nula.

literal

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

La longitud y la capacidad predeterminadas de los segmentos creados por literales son iguales. Cabe señalar que si especificamos el valor de un índice por separado, si el elemento anterior al valor del índice no se declara, será el tipo de valor predeterminado de segmento. .

hacer metodo

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

La longitud y la capacidad de la rebanada se pueden especificar por marca.

método de interceptación

Los sectores se pueden obtener de matrices u otros sectores. En este momento, el nuevo sector compartirá un conjunto subyacente con el antiguo conjunto o sector. No importa quién modifique los datos, afectará al conjunto subyacente, pero si el nuevo sector se expande , Entonces las matrices subyacentes no son las mismas.

s[:]

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

El sector obtenido por es: equivalente a una referencia a todo el sector.[0,len(a)-1]

si:]

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

imagen.png

s2 := s1[1:3:4]

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

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

imagen.png

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

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

imagen.png

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

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

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

Se encontrará que el valor del segmento original no ha cambiado. Esto se debe a que usamos agregar. Después de agregar, la capacidad del segmento original no es suficiente. En este momento, se copiará una nueva matriz. De hecho, los parámetros de la función de go se pasan, solo por valor, no por referencia. Cuando los datos subyacentes de la división no han cambiado, la forma de modificarlos afectará la matriz subyacente original. Cuando la división se expanda, será una nueva matriz. después de la expansión, entonces, ¿cómo modificar esto? La nueva matriz no afectará a la matriz original.

¿Se pueden comparar arrays y slices?

Solo se pueden comparar matrices de la misma longitud y tipo

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)

rebanada solo se puede comparar con cero, el resto no se puede comparar

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

Pero debe tenerse en cuenta que dos rebanadas que son nulas no se pueden comparar, solo se pueden comparar con nil, donde nil es el verdadero 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 busca "Pretende entender la programación"

Supongo que te gusta

Origin juejin.im/post/7121628307040403487
Recomendado
Clasificación