Is slice really a reference type?

From the public account: New World Grocery Store


Note : The following only represents personal opinion, if it is inconsistent with the content of most articles, please refer to it as appropriate

Three Questions for the Soul

Q1. What is the difference between reference and pointer?

Answer: Actually, I don’t know the difference between them, but they all have one thing in common, that is, they can point to the real value, and operating them will change the real value.

Q2. When there are pointers in go, why do we have to put forward the concept of reference?

Answer: Obviously I can’t answer this question. To be honest, I just read many articles saying that. I haven’t studied it myself.

Q3. Is there really a reference to this concept in go?

Answer: This question may be able to answer Q2. But I dare not give an affirmative answer. I took this question to the official document and searched it, and found that I did not search for words related to "quotation", nor did I find references to variables. definition

Thoughts from the Three Questions of the Soul

The core of the three questions of the soul is the reference, so at this time I started to think about which are the reference types in go. As we all know, in most articles, slices are mentioned as reference types. I have always believed that slices are reference types, but until I encountered the following code, I began to doubt my own perception.

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

The above code appears very frequently in normal development, so my question is that since the slice is a reference type, then why must be assigned to itself after append.

Analysis of append

If we don’t assign a value to itself, what will happen?
Let’s take a look at the following code

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

This result is obviously inconsistent with that the slice is a reference type. What happened in the middle, and what should we do? Next, we let the code speak. At this point, the simple code above has no more information. Then we try to look at the compiled assembly code, hoping to find clues from the assembly.

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

Some of the key assembly codes are as follows:

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)

The compilation of go is the compilation of Plan9. And I can hardly understand, but luckily I found some of the keywords.

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)And a := make([]int64, 0), (test.go:7) CALL runtime.growslice(SB)and _ = append(a, 10)can all correspond. Finally, I slice.gofound these two functions in the files in the runtime package of the go source code , and found the following structure

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

When the append function is called runtime.growslice, it is also called in the assembly code. The signature of the growslice function is as follows

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

We pay attention to the following debug call stack (the tips of debug runtime will be introduced later):
Insert picture description here

From the above figure, in this column, append called growsliceand returned a new slice structure in the runtime layer , but I did not assign the returned variable to the original variable a, so the printed result was empty. At this point, I feel that the problem is basically a bit clue, because the return is a structure, so it must be assigned, otherwise the original structure variable will not change.

Then the new question is coming. If we don't assign to the original slice after append, will the original slice remain unchanged?

Judging from the initial code output result, the original slice does not have any changes, but the real result is still let us use the code to speak

// 长度取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

From the above code, we know that after the append function is operated, the underlying array has actually changed, just because the result after append is not assigned to the variable a, so the len in the structure has not changed, resulting in the inability to print the slice content correctly

in conclusion

  1. Slice in go is actually a slicestructure in runtime
  2. After the append operation slices the underlying array, the length or capacity of the array is changed, so it needs to be re-assigned to the original slice. If the slice is expanded, the original array address will also change.
  3. Based on the above, I arbitrarily concluded that there is no concept of reference in go, mainly pointers, so there is only value transfer in go

supplement

slice is a private structure containing data, cap and len

map is a pointer to the runtime.hmap structure

chan is a pointer to the runtime.hchan structure

Q1: runtime.makeslice returns an unsafe.Pointer?

This unsafe.Pointer is the address of the first element of the array in the slice

Q2: Some assembly code append does not call runtime.growslice?

The Go compiler is partially optimized. If the slice capacity is sufficient, runtime.growslice does not need to be called. If the capacity is insufficient, runtime.growslice will be called for expansion. Please refer to runtime/slice.go for specific expansion logic.

Note: At the time of writing this article, the go version used by the author is: go1.13.4

reference

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

Guess you like

Origin blog.csdn.net/u014440645/article/details/108569131