Recently, I was working on a project with a colleague. I had to write a lot of data to a Map that was passed into the function as a parameter. He asked me a question: “If I pass Map as a function argument, is it as weird as if I pass Slice, and do I have to return Map as a return value so that the Map variable outside the function can see the data added here?”
What do you mean, is it as weird as using Slice? In fact, I can already guess what he means by saying that the underlying array of Slice will be expanded so that two Slice variables that used to point to the same underlying array inside and outside the function will point to two different underlying arrays.
As a result, data is added inside the function, but the original Slice variable outside the function does not change at all. For example, there is a program that looks like this.
func main() {
s := []int{1, 2, 3}
reverse(s)
fmt.Println(s)
}
func reverse(s []int) {
s = append(s, 999, 1000, 1001)
for i, j := 0, len(s)-1; i < j; i++ {
j = len(s) - (i + 1)
s[i], s[j] = s[j], s[i]
}
}
Copy the code
The original slice had only three elements, which were 1, 2 and 3. We assigned the slice to the variable S, and then passed the variable s as a parameter to the function Reverse for processing. The function Reverse added several values to the original slice before reversing the slice elements, which led to the slice expansion. Because a slice is not actually a pointer type, its runtime type representation is SliceHeader.
type SliceHeader struct {
Data uintptr
Len int
Cap int
}
Copy the code
Because of the value passing rule in Go, a SliceHeader structure will be copied inside the function when slicing is used as a parameter, but the Data pointer of the structure will be the same as the Data pointer of the external slice, which is the same underlying Data.
This causes the Data pointer in the SliceHeader inside the function to change and the original slice outside the function to point to the original underlying array. As a result, the output of the slice variable outside the printing function is [1, 2, 3], but the slice inside the function is [1001, 1000, 999, 3, 2, 1].
The figure below shows how the underlying array pointed to by the inner and outer slices of the function changes.
Is there a problem with using Map as a function parameter? Ah, I’m going to make fun of the fact that everything is a pass-through design, which makes some programmers who write Go nervous and wonder if the underlying layer will change when they use maps and structure Pointers as parameters.
Of course, I am not blindly confident when WRITING Go. When I use something written in books or other people’s articles, IF I am not sure whether they are right or not, I will write a list to test it. Look for the data that explains these knowledge points again after the event, oneself solve puzzle.
If Map is used as a function parameter, Map variables inside and outside the function will still point to the same underlying memory. Why is that? I found the answer in the hash table chapter of Go Language Design and Implementation, page 75 if you have a book.
If you don’t have a book, you can see the online book address posted in the reference link at the end of the article.
The Map initialization is described as follows
To create hashes with make, the Go compiler will convert them to Runtime. makemap during type checking. Initializing hashes with literals is just a tool provided by the language.
func makemap(t *maptype, hint int, h *hmap) *hmap {
mem, overflow := math.MulUintptr(uintptr(hint), t.bucket.size)
if overflow || mem > maxAlloc {
hint = 0
}
......
return h
}
Copy the code
From the above explanation and code we know that the Map data type is actually a pointer to the HMAP type at runtime, but it is hidden while we are writing the code.
Since a Map variable is actually a pointer variable, this is completely different from Slice. Although Pointers are passed by value in Go as function parameters, both Pointers point to the same memory in the HMAP structure. Hmap contains many fields. We only need to know fields of the pointer type buckets and oldbuckets.
type hmap struct {
count int
flags uint8
B uint8
noverflow uint16
hash0 uint32
buckets unsafe.Pointer
oldbuckets unsafe.Pointer
nevacuate uintptr
extra *mapextra
}
Copy the code
Buckets (Bmaps), the structure used to store key-value pair data in Go’s Map, we won’t dig too deeply into BMaps.
Buckets points to an array of buckets. When the hash table grows to the point where it needs to be expanded, Go doubles the number of buckets to create a new bucket array. The old data is stored in the bucket pointed by oldbuckets and then migrated to the new bucket when accessed.
As a result, the Map has the address of the bucket array, which is stored in the BUCKETS field of HMAP. Changing the value of the bucket field does not affect the MEMORY address of HMAP.
So when the Map is expanded due to operations inside the function, there is no weird phenomenon of Slice pointing to different underlying arrays as in the above example.
I don’t know if you can understand my analysis here, but this article is actually a record of my own thinking, in case I forget it after a long time. Pass-by-value and pass-by-reference are different in different languages, and for guys like us who know at least three programming languages 🙂 it’s just a matter of writing notes to prevent confusion.
(I believe most people can’t build their careers on a single programming language.)
In addition, I think the use of Go Slice does require a little mental effort, and it is easy to step into a pit if you do not pay attention to it. After a long time, people will start to doubt themselves when using Map and pointer as parameters. I hope this article will help to solve your doubts.
Refer to the address
- Drap. Me/Golang/Docs…