13 Parameter Passing the Differences Between Passing by Value, Reference, and Pointer

13 Parameter Passing - The Differences Between Passing by Value, Reference, and Pointer #

In the previous lesson, I left a thinking question about pointers to interfaces. In “Lesson 6 | struct and interface: what functionalities do structures and interfaces implement?”, you learned how to implement an interface and that if a value receiver implements an interface, then a pointer to that value also implements the interface. Now let’s review the knowledge of interface implementation together and then answer the question about pointers to interfaces.

In the code below, the value type address implements the fmt.Stringer interface as the receiver, so its pointer type *address also implements the fmt.Stringer interface.

ch13/main.go

    type address struct {
       province string
       city string
    }
    
    func (addr address) String()  string{
       return fmt.Sprintf("the addr is %s%s",addr.province,addr.city)
    }

In the code example below, I defined a variable add of value type and then passed both the variable itself and its pointer &add as arguments to the function printString. It is found that both cases are allowed, and the code can run successfully. This also proves that when a value type implements an interface as the receiver, its pointer type also implements the same interface.

ch13/main.go

    func main() {
       add := address{province: "北京", city: "北京"}
       printString(add)
       printString(&add)
    }
    
    func printString(s fmt.Stringer) {
       fmt.Println(s.String())
    }

Based on the above conclusion, let’s continue analyzing whether it is possible to define a pointer to an interface. It is as follows:

ch13/main.go

    var si fmt.Stringer = address{province: "上海",city: "上海"}
    printString(si)
    sip := &si
    printString(sip)

In this example, because the type address has implemented the interface fmt.Stringer, its value can be assigned to the variable si, and si can also be passed as an argument to the function printString.

You can then use the operation sip := &si to obtain a pointer to the interface, which is OK. However, you cannot pass the pointer to the interface sip as an argument to the function printString in the end. The Go compiler will prompt you with the following error message:

./main.go:42:13: cannot use sip (type *fmt.Stringer) as type fmt.Stringer in argument to printString:
	*fmt.Stringer is pointer to interface, not interface

So, as a summary: although a pointer to a concrete type can implement an interface, a pointer to an interface can never implement that interface.

Therefore, you almost never need a pointer to an interface. Forget about it and do not let it appear in your code.

Through this thinking question, I believe you also have a certain understanding of the concepts of value types, reference types, and pointers in Go, but there may still be some confusion. In this lesson, I will analyze these concepts in more depth.

Modifying Parameters #

Suppose you define a function and modify the parameter inside the function, and you want the caller to be able to get the latest modified value through the parameter. I will continue to use the person struct that we used in the previous course as an example, as shown below:

ch13/main.go

    func main() {
       p := person{name: "张三", age: 18}
       modifyPerson(p)
       fmt.Println("person name:", p.name, ", age:", p.age)
    }
    
    func modifyPerson(p person) {
       p.name = "李四"
       p.age = 20
    }
    
    type person struct {
       name string
       age  int
    }
func main() {
    p := person{name: "张三", age: 18}
    fmt.Printf("main函数:p的内存地址为%p\n", &p)
    modifyPerson(&p)
    fmt.Println("person name:", p.name, ",age:", p.age)
}

你需要在代码中添加必要的包引用和函数定义。这段代码的目的是在调用 modifyPerson 函数时传递变量 p 的指针。运行此代码,输出结果如下:

main函数:p的内存地址为0xc0000a6020
modifyPerson函数:p的内存地址为0xc0000a6020
person name: 李四 ,age: 20

你会发现,在 main 函数和 modifyPerson 函数中,变量 p 的内存地址都是相同的。这意味着它们引用的是同一个内存地址,即指针指向的内存。因此,在 modifyPerson 函数中修改参数 p 的值会修改原始变量 p 的值。

指针类型的变量是一种特殊的值类型,它的值是另一个变量的内存地址。在 Go 语言中,通过传递指针类型的参数,可以在函数中修改原始变量的值。

func modifyPerson(p *person) {
   fmt.Printf("modifyPerson function: the memory address of p is %p\n", p)
   p.name = "李四"
   p.age = 20
}

If you run this example, you will find that the printed memory addresses are the same, and the data has been successfully modified as well, as shown below:

main function: the memory address of p is 0xc0000a6020
modifyPerson function: the memory address of p is 0xc0000a6020
person name: 李四, age: 20

Therefore, a pointer type parameter can always modify the original data because the memory address is passed during parameter passing.

Tip: When passing by value, a pointer is also a memory address. By using a memory address, you can find the memory block of the original data, so modifying it is equivalent to modifying the original data.

Reference Types #

Next, I will introduce reference types, including maps and channels.

Maps #

In the previous example, if I don’t use a custom person structure and pointer, can I achieve the same modification using a map?

Let’s try it out as shown below:

ch13/main.go

func main() {
   m := make(map[string]int)
   m["飞雪无情"] = 18
   fmt.Println("The age of 飞雪无情 is", m["飞雪无情"])
   modifyMap(m)
   fmt.Println("The age of 飞雪无情 is", m["飞雪无情"])
}

func modifyMap(p map[string]int) {
   p["飞雪无情"] = 20
}

I defined a variable m of type map[string]int and stored a key-value pair with the key as "飞雪无情" and the value as 18. Then, I passed this variable m to the modifyMap function. The modifyMap function simply modifies the corresponding value to 20. Now, after running this code, let’s check if the modification was successful through the print output. The result is as follows:

The age of 飞雪无情 is 18
The age of 飞雪无情 is 20

Indeed, the modification was successful. Are you confused? We didn’t use a pointer, just a map type parameter. According to the value passing principle of Go, the map inside the modifyMap function is a copy, so how is it possible to modify it successfully?

To answer this question, let’s start with the make function, which is a built-in function in Go. In Go, any code that creates a map (whether it is a literal or the make function) ultimately calls the runtime.makemap function.

Tip: Creating a map using a literal or the make function results in a call to the runtime.makemap function. This conversion is automatically done by the Go compiler.

From the code below, you can see that the makemap function returns a *hmap type, which means it returns a pointer. Therefore, the map we create is actually a *hmap.

src/runtime/map.go

// makemap implements Go map creation for make(map[k]v, hint).
func makemap(t *maptype, hint int, h *hmap) *hmap {
   // omitted irrelevant code
}

Because the map type in Go is essentially a *hmap, according to the substitution principle, the modifyMap(p map) function I just defined is actually modifyMap(p *hmap). Isn’t this the same as the pointer type parameter call discussed in the previous section? This is also the reason why the original data can be modified through the map type parameter, because it is essentially a pointer.

To further verify that the created map is indeed a pointer, I modified the above example to print the memory address of the map variable and the corresponding parameter, as shown in the code below:

func main(){

  // omit other unchanged code

  fmt.Printf("main function: the memory address of m is %p\n", m)

}

func modifyMap(p map[string]int) {

   fmt.Printf("modifyMap function: the memory address of p is %p\n", p)

   // omit other unchanged code

}

The two print statements in the example are new additions, and the rest of the code remains unchanged. I won’t copy the other code here. When running the modified program, you can see the following output:

The age of "Flying Snow" is 18
main function: the memory address of m is 0xc000060180
modifyMap function: the memory address of p is 0xc000060180
The age of "Flying Snow" is 20

From the output, you can see that their memory addresses are exactly the same, which is why the original data can be modified, resulting in an age of 20. And when printing the pointers, I directly used the variables m and p, without the need to use the address-of operator &, because they are pointers themselves, so there is no need to use & to take the address.

Therefore, in Go, the creation of a map is simplified for us by the make function or the literal syntax, making it easier for us to use maps. It is actually just syntactic sugar, which is a long-standing tradition in the programming world.

Note: The map here can be understood as a reference type, but in essence, it is a pointer. When passing it as an argument, it is still pass-by-value, not the so-called pass-by-reference in other programming languages.

chan #

Do you remember the channel we learned in the Go concurrency module? It can also be understood as a reference type, and it is essentially a pointer.

By looking at the source code below, you can see that the created chan is actually a *hchan, so it behaves the same way as a map in parameter passing.

func makechan(t *chantype, size int64) *hchan {

    // omit irrelevant code

}

Strictly speaking, Go doesn’t have reference types, but we can refer to maps and channels as reference types for ease of understanding. In addition to maps and channels, functions, interfaces, and slice types in Go can also be referred to as reference types.

Pro tip: Pointer types can also be understood as a kind of reference type.

Zero Values of Types #

In Go, when defining a variable, it can be done either through declaration or through the make and new functions. The difference is that the make and new functions are explicit declarations with initialization. If we declare a variable without explicit initialization, then the default value of that variable is the zero value of the corresponding type.

From the table below, you can see that the zero value of reference types (referred to as reference types for convenience here) is nil.

112.png

(Zero values of various types)

Summary #

In Go, function parameters are passed by value, and the passed arguments are copies of the original data. If the copied content is of value type, then the original data cannot be modified in the function; if the copied content is a pointer (or can be understood as a reference type such as a map or channel), then the original data can be modified in the function.

Lark20201209-184447.png

So when creating a function, we need to decide the type of the parameter based on our actual needs, in order to better serve our business logic.

In this lesson, I didn’t provide an example when explaining chan, so you can try to define a function with a chan parameter as an exercise.

In the next lesson, I will talk about “Memory Allocation: new or make? When to use each?”. Remember to tune in!