面试官问:Go 中的参数传递是值传递还是引用传递?
A program divides variables into variable names and variable contents. The storage of variable contents is typically allocated on the heap or stack. In Go, there are two ways to pass variables: value passing and reference passing. Value passing directly copies the variable's content, while reference passing passes the address of the variable's content.
Golang How Does It Work
When asked, "How are parameters passed in Go?" the answer is straightforward. In Golang, all types are passed by value, not reference. Even pointer types are passed by copying the pointer. For data structures wrapped at the underlying level, the copy operation only copies the instance pointer, not the underlying data pointer.
Here's an example with Go version 1.8's slice:
func makeslice(et *_type, len, cap int) unsafe.Pointer {
mem, overflow := math.MulUintptr(et.size, uintptr(cap))
if overflow || mem > maxAlloc || len < 0 || len > cap {
mem, overflow := math.MulUintptr(et.size, uintptr(len))
if overflow || mem > maxAlloc || len < 0 {
panicmakeslicelen()
}
panicmakeslicecap()
}
return mallocgc(mem, et, true) // Allocate memory
}
The slice initializes by calling the runtime's makeslice function, which returns the slice's address to the receiving variable.
type slice struct {
array unsafe.Pointer // Pointer to the underlying array
len int
cap int
}
// Initialization process
p := make([]int, 0)
fmt.Printf("Variable p's address: %p", &p)
fmt.Printf("Slice address: %p\n", p)
The output shown is due to Go's automatic dereferencing (Go's internal dereferencing operation). During automatic dereferencing, the receiver converts from a pointer type to a value type. Conversely, when autounwrapping is involved, the receiver converts from a value type to a pointer type.

What happens if automatic dereferencing is not implemented? Here's an example of the scenario without automatic dereferencing:
// When printing variable p, the actual process involves dereferencing
// &p retrieves the address, * retrieves the value from the address
// 1. Get pointer address &p
// 2. Get array address &((&p).array)
// 3. Get bottom array content *&((&p).array)
The function passing process without automatic borrowing also uses copying the pointer, as shown in the image:

package main
import (
"fmt"
)
func change(p1 []int) {
fmt.Printf("p1's memory address: %p\n", &p1) // p1's memory address: 0xc0000a6048
fmt.Printf("Memory address of slice received: %p\n", p1) // Memory address of slice received: 0xc00008c030
p1 = append(p1, 30)
}
func main() {
p := make([]int, 3) // Returns a pointer
p = append(p, 20)
fmt.Printf("p's memory address: %p\n", &p) // p's memory address: 0xc00009a018
fmt.Printf("Slice's memory address: %p\n", p) // Slice's memory address: 0xc00008c030
change(p) // Reassigns p1 to point to the slice's address
fmt.Printf("Modified p's memory address: %p\n", &p) // Modified p's memory address: 0xc00009a018
fmt.Printf("Modified slice's memory address: %p\n", p) // Modified slice's memory address: 0xc00008c030
fmt.Println("Modified slice:", p) // Modified slice: [0 0 0 20]
fmt.Println(*&p) // Modified slice: [0 0 0 20]
}
Important note: During function passing, the pointer to the array is not copied, but the pointer to the slice's address is copied in the makeslice function.
Source Code Implementation
Some older articles mention that make returns a slice instance. However, this statement is outdated. Starting from Go 1.2, make returns the pointer to the instance rather than the instance itself.
GitHub repository: https://github.com/golang/go/commits/dev.boringcrypto.go1.12/src/runtime/slice.go


Extensions
Similar to slices, maps and channels also follow the same principles.
Map definition: "Go provides a built-in map type that implements a hash table. Map types are reference types, like pointers or slices." Chan and map are also pointers, so their principles are similar to slices.
func makemap(t *maptype, hint int, h *hmap) *hmap {
mem, overflow := math.MulUintptr(uintptr(hint), t.bucket.size)
if overflow || mem > maxAlloc {
hint = 0
}
...
}
func makechan(t *chantype, size int) *hchan {
...
mem, overflow := math.MulUintptr(elem.size, uintptr(size))
if overflow || mem > maxAlloc - hchanSize || size < 0 {
panic(plainError("makechan: size out of range"))
}
...
}
If not paying attention to details, unnecessary problems may arise, such as:
package main
import "fmt"
type InfoIns struct {
Name string
info []string
}
func NewInfoIns() InfoIns{
return InfoIns{
Name: "",
info: nil,
}
}
func (n *InfoIns) SetInfo(info []string){
n.info = info
}
func main(){
infoIns := NewInfoIns()
info := []string{"p1", "p2", "p3"}
infoIns.SetInfo(info)
info[1] = "p4"
fmt.Println(infoIns.info) // [p1 p4 p3]
}
The InfoIns stores the address of the info after SetInfo. Any modifications to info will also modify InfoIns. The solution is to re-allocate the address in SetInfo.
func (n *InfoIns) SetInfo(info []string){
n.info = make([]string, len(info))
copy(n.info, info)
}
Footnotes
Use Goland to view Go source code: Ctrl+Shift+f for global search, select ALL PLACE in Scope.

