¿Es slice realmente un tipo de referencia?

De la cuenta pública: New World Grocery Store


Nota : lo siguiente solo representa una opinión personal; si no es coherente con el contenido de la mayoría de los artículos, consúltelo según corresponda

Tres preguntas para el alma

P1: ¿Cuál es la diferencia entre referencia y puntero?

Respuesta: En realidad, no conozco la diferencia entre ellos, pero todos tienen una cosa en común, es decir, pueden señalar el valor real, y operarlos cambiará el valor real.

P2: Cuando hay punteros en marcha, ¿por qué tenemos que presentar el concepto de referencia?

Respuesta: Obviamente no puedo responder a esta pregunta. Para ser honesto, acabo de leer muchos artículos que dicen eso. No lo he estudiado yo mismo.

P3: ¿Existe realmente una referencia a este concepto en marcha?

Respuesta: Esta pregunta puede responder a la P2. Pero no me atrevo a dar una respuesta afirmativa. Llevé esta pregunta al documento oficial y lo busqué, y descubrí que no busqué palabras relacionadas con "cita", ni encontré referencias a variables. definición

Pensamientos de las tres preguntas del alma

El núcleo de las tres preguntas del alma es la referencia, así que en este momento comencé a pensar cuáles son los tipos de referencia en go. Como todos sabemos, en la mayoría de los artículos, los sectores se mencionan como tipos de referencia. Siempre he creído que los sectores son tipos de referencia, pero hasta que encontré el siguiente código, comencé a dudar de mi propia percepción.

a := make([]int64, 0)
a = append(a, 10)

El código anterior aparece con mucha frecuencia en el desarrollo normal, por lo que mi pregunta es que, dado que el segmento es un tipo de referencia, por qué debe asignarse a sí mismo después de agregarlo.

Análisis de anexo

Si no nos asignamos un valor a sí mismo, ¿qué pasará?
Veamos el siguiente código

a := make([]int64, 0, 0)
_ = append(a, 10)
fmt.Println(a)
fmt.Printf("address: %p\n", a)
// 输出:
[]
address: 0x1195a98

Este resultado es obviamente inconsistente con que el segmento es un tipo de referencia. ¿Qué pasó en el medio y qué debemos hacer? A continuación, dejamos que el código hable. En este punto, el código simple mencionado anteriormente no tiene más información. Luego tratamos de mirar el código ensamblador compilado, esperando encontrar pistas del ensamblado.

# go代码转汇编
go tool compile -N -l -S test.go

Algunos de los códigos de ensamblaje clave son los siguientes:

0x002f 00047 (test.go:6)	PCDATA	$0, $1
0x002f 00047 (test.go:6)	PCDATA	$1, $0
0x002f 00047 (test.go:6)	LEAQ	type.int64(SB), AX
0x0036 00054 (test.go:6)	PCDATA	$0, $0
0x0036 00054 (test.go:6)	MOVQ	AX, (SP)
0x003a 00058 (test.go:6)	XORPS	X0, X0
0x003d 00061 (test.go:6)	MOVUPS	X0, 8(SP)
0x0042 00066 (test.go:6)	CALL	runtime.makeslice(SB)
0x0047 00071 (test.go:6)	PCDATA	$0, $1
0x0047 00071 (test.go:6)	MOVQ	24(SP), AX
0x004c 00076 (test.go:6)	PCDATA	$1, $1
0x004c 00076 (test.go:6)	MOVQ	AX, "".a+112(SP)
0x0051 00081 (test.go:6)	XORPS	X0, X0
0x0054 00084 (test.go:6)	MOVUPS	X0, "".a+120(SP)
0x0059 00089 (test.go:7)	JMP	91
0x005b 00091 (test.go:7)	PCDATA	$0, $2
0x005b 00091 (test.go:7)	LEAQ	type.int64(SB), CX
0x0062 00098 (test.go:7)	PCDATA	$0, $1
0x0062 00098 (test.go:7)	MOVQ	CX, (SP)
0x0066 00102 (test.go:7)	PCDATA	$0, $0
0x0066 00102 (test.go:7)	MOVQ	AX, 8(SP)
0x006b 00107 (test.go:7)	XORPS	X0, X0
0x006e 00110 (test.go:7)	MOVUPS	X0, 16(SP)
0x0073 00115 (test.go:7)	MOVQ	$1, 32(SP)
0x007c 00124 (test.go:7)	CALL	runtime.growslice(SB)
0x0081 00129 (test.go:7)	PCDATA	$0, $1
0x0081 00129 (test.go:7)	MOVQ	40(SP), AX
0x0086 00134 (test.go:7)	JMP	136
0x0088 00136 (test.go:7)	PCDATA	$0, $0
0x0088 00136 (test.go:7)	MOVQ	$10, (AX)

La compilación de go es la compilación de Plan9. Y apenas puedo entender, pero afortunadamente encontré algunas de las palabras clave.

0x0042 00066 (test.go:6)	CALL	runtime.makeslice(SB)
0x007c 00124 (test.go:7)	CALL	runtime.growslice(SB)

(test.go:6) CALL runtime.makeslice(SB)Y a := make([]int64, 0), (test.go:7) CALL runtime.growslice(SB)y _ = append(a, 10)todos pueden corresponder. Finalmente, slice.goencontré estas dos funciones en los archivos del paquete de tiempo de ejecución del código fuente de go , y encontré la siguiente estructura

// 其中array 为一个指向数组的地址
// len 表示当前数组的长度
// cap 表示当前数组的真实容量
type slice struct {
    
    
	array unsafe.Pointer
	len   int
	cap   int
}

Cuando se llama a la función append runtime.growslice, también se llama en el código ensamblador. La firma de la función growlice es la siguiente

func growslice(et *_type, old slice, cap int) slice

Prestamos atención a la siguiente pila de llamadas de depuración (los consejos del tiempo de ejecución de depuración se presentarán más adelante):
Inserte la descripción de la imagen aquí

De la figura anterior, en esta columna, agregue la llamada growslicey devolvió una nueva estructura de corte en la capa de tiempo de ejecución , pero no asigné la variable devuelta a la variable original a, por lo que el resultado impreso estaba vacío. En este punto, siento que el problema es básicamente una pequeña pista, porque la devolución es una estructura, por lo que debe asignarse, de lo contrario, la variable de estructura original no cambiará.

Entonces viene la nueva pregunta: si no asignamos al segmento original después de agregar, ¿el segmento original permanecerá sin cambios?

A juzgar por el resultado de salida del código inicial, el segmento original no tiene ningún cambio, pero el resultado real sigue siendo que usemos el código para hablar

// 长度取1, 是为了保证能够正常取到底层数组的地址
// 容量取2,是为了保证数组容量足够, 而不用对数组进行扩容导致地址发生变化
a := make([]int64, 1, 2)
_ = append(a, 10)
fmt.Println(a)
// 强制访问元素a[2]
baseAddr := unsafe.Pointer(&a[0])
offset := unsafe.Sizeof(a[0])
fmt.Println(*(*int64)(unsafe.Pointer(uintptr(baseAddr) + offset)))
// 输出:
[0]
10

A partir del código anterior, sabemos que después de que se opera la función de agregar, la matriz subyacente ha cambiado realmente, solo porque el resultado después de agregar no se asigna a la variable a, por lo que el len en la estructura no ha cambiado, lo que resulta en la imposibilidad de imprimir el contenido del segmento correctamente.

En conclusión

  1. Slice in go es en realidad una sliceestructura en tiempo de ejecución
  2. Después de que la operación de adición corta la matriz subyacente, la longitud o la capacidad de la matriz cambia, por lo que debe ser reasignada a la porción original. Si la porción se expande, la dirección de la matriz original también cambiará.
  3. Con base en lo anterior, concluí arbitrariamente que no existe un concepto de referencia en go, principalmente punteros, por lo que solo hay transferencia de valor en go

suplemento

slice es una estructura privada que contiene datos, cap y len

map es un puntero a la estructura runtime.hmap

chan es un puntero a la estructura runtime.hchan

P1: runtime.makeslice devuelve un puntero inseguro?

Este puntero inseguro es la dirección del primer elemento de la matriz en el segmento

P2: Algún código de ensamblaje adjunto no llama a runtime.growslice?

El compilador Go está parcialmente optimizado. Si la capacidad del segmento es suficiente, no es necesario llamar a runtime.growslice. Si la capacidad es insuficiente, se llamará a runtime.growslice para la expansión. Consulte runtime / slice.go para obtener una lógica de expansión específica.

Nota: En el momento de escribir este artículo, la versión go utilizada por el autor es: go1.13.4

referencia

https://draveness.me/golang/docs/part2-foundation/ch05-keyword/golang-make-and-new/

Supongo que te gusta

Origin blog.csdn.net/u014440645/article/details/108569131
Recomendado
Clasificación