12 Pointers Explained When Should Pointers Be Used

12 Pointers Explained - When Should Pointers Be Used #

In this lesson, I will guide you through the third module of this column: In-depth Understanding of Go Language. This part will mainly explain the advanced features of Go language and the underlying principles of some specific functionalities. Through this part of the learning, you will not only be able to better use Go language, but also have a deeper understanding of Go language, such as how the underlying implementation of slice, which you use, works.

What is a Pointer #

We all know that during program execution, data is stored in memory. Memory is abstracted as a series of storage spaces with consecutive numbers, and each piece of data stored in memory will have an ID, which is called a memory address. With this memory address, we can find the data stored in the memory, and this memory address can be assigned to a pointer.

Pro tip: Memory addresses are usually represented by hexadecimal numbers, such as 0x45b876.

In summary, in programming languages, a pointer is a data type used to store a memory address pointing to an object stored in that memory. This object can be a string, an integer, a function, or a custom structure.

Pro tip: You can also simply understand a pointer as a memory address.

Here’s an analogy: in every book, there is a table of contents, and the table of contents contains the page numbers corresponding to each chapter. You can think of the page numbers as a series of memory addresses. Using the page numbers, you can quickly locate a specific chapter (that is, you can quickly find the stored data using the memory address).

Declaration and Definition of Pointers #

In Go language, it is very easy to obtain the pointer of a variable by using the address operator &, as shown in the example below:

ch12/main.go

func main() {

   name := "飞雪无情"

   nameP := &name // Take the address

   fmt.Println("The value of the name variable is:", name)

   fmt.Println("The memory address of the name variable is:", nameP)

}

In the example, I defined a string variable name with the value “飞雪无情”. Then I used the address operator & to get the memory address of the variable name and assigned it to the pointer variable nameP, which has a type of *string. When running the above example, you will see the following output:

The value of the name variable is: 飞雪无情

The memory address of the name variable is: 0xc000010200

The 0xc000010200 is the memory address. This memory address can be assigned to the pointer variable nameP.

Pointers are very inexpensive and only occupy 4 or 8 bytes of memory.

In the above example, the pointer nameP has a type of *string, which is used to point to data of type string. In Go language, you can represent the corresponding pointer type by adding an asterisk * before the type name. For example, the pointer type of int is *int, the pointer type of float64 is *float64, and the pointer type of a custom structure A is *A. In general, a pointer type is obtained by adding an asterisk * before the corresponding type.

Now, let me use a diagram to help you better understand the relationship between ordinary variables, pointer variables, memory addresses, and memory.

Drawing 1.png

(The diagram illustrating pointer variables and memory addresses)

The diagram above corresponds to the example I just mentioned. From the diagram, you can see that the value of the ordinary variable name, which is “飞雪无情”, is stored in the memory block with the memory address 0xc000010200. The pointer variable is also a variable, and it also needs a memory space to store values. The value stored in this memory space is 0xc000010200. I believe you have already seen the key point here: the value of the pointer variable nameP is exactly the memory address of the ordinary variable name, thus establishing a pointing relationship.

Pro tip: The value of a pointer variable is the memory address it points to, while the value of an ordinary variable is the data we store.

Pointers of different types cannot be assigned to each other. For example, you cannot take the address of a string variable and assign it to a pointer of type *int. The compiler will prompt you with Cannot use '&name' (type *string) as type *int in assignment.

In addition, in addition to declaring a pointer variable using a short declaration syntax, you can also declare it using the var keyword. For example, var intP *int declares a variable intP of type *int.

var intP *int

intP = &name // Pointers of different types cannot be assigned

As you can see, like ordinary variables, pointer variables can be declared using the var keyword or the short declaration syntax.

Pro tip: Pointer variables declared using var cannot be directly assigned or dereferenced because at that time they are just variables without corresponding memory addresses. Their values are nil.

Unlike ordinary types, pointer types can also be declared using the built-in new function, as shown below:

intP1 := new(int)

The built-in new function has one parameter, and you can pass a type to it. It will return the corresponding pointer type. For example, in the above example, it will return a pointer of type *int, which is intP1.

Pointer Operations #

In Go language, there are two main operations for pointers: getting the value pointed to by a pointer and modifying the value pointed to by a pointer.

First, let’s talk about how to get the value pointed to by a pointer. I will demonstrate with the following code:

nameV := *nameP

fmt.Println("The value pointed to by nameP is:", nameV)

As you can see, to get the value pointed to by a pointer, simply add an asterisk * before the pointer variable. The resulting variable nameV will have the value “飞雪无情”. The method is quite simple.

Modifying the value pointed to by a pointer is also very simple. For example, in the code below:

*nameP = "公众号:飞雪无情" // Modifying the value pointed to by the pointer

fmt.Println("The value pointed to by nameP is:", *nameP)

fmt.Println("The value of the name variable is:", name)

Assigning a value to *nameP means modifying the value pointed to by the pointer nameP. When running the program, you will see the following output: nameP pointer points to: 公众号:飞雪无情

Value of name variable is: 公众号:飞雪无情

From the printed result, we can see that not only the value pointed to by the nameP pointer has been changed, but also the value of the name variable has been changed. This is the function of pointers. Because the memory storing the data of the variable name is the same as the memory pointed to by the pointer nameP, when this memory is modified by nameP, the value of the variable name is also modified.

We already know that pointer variables directly defined using the var keyword cannot be assigned a value because their value is nil, which means they do not yet point to a memory address. For example, in the following example:

var intP *int

*intP = 10

It will prompt “invalid memory address or nil pointer dereference” when running. What should you do in this case? You just need to allocate a block of memory to it using the new function, as shown below:

var intP *int = new(int)

// Alternatively, you can use the short declaration, here is for demonstration purposes only

// intP := new(int)

Pointer Parameters #

Suppose there is a function modifyAge that is used to modify the age, as shown in the code below. However, when running it, you will see that the value of age has not been modified and is still 18, it has not become 20.

age := 18

modifyAge(age)

fmt.Println("The value of age is:", age)

func modifyAge(age int) {
    age = 20
}

The reason for this is that the age in modifyAge is just a copy of the actual argument age, so modifying it does not change the value of the actual argument age.

If you want to achieve the goal of modifying the age, you need to use a pointer. Now let’s modify the previous example as shown below:

age := 18

modifyAge(&age)

fmt.Println("The value of age is:", age)

func modifyAge(age *int) {
    *age = 20
}

In other words, when you need to change the value of the actual argument through a formal parameter in a function, you need to use a pointer type parameter.

Pointer Receivers #

The pointer receiver is explained in detail in [“Lesson 6| struct and interface: What functionalities do structures and interfaces implement?”] You can review it again. There are several considerations regarding whether to use a pointer type as a receiver:

  1. If the receiver type is a reference type such as map, slice, or channel, do not use a pointer.
  2. If you need to modify the receiver, then use a pointer.
  3. If the receiver is a large type, you can consider using a pointer because memory copying is inexpensive, so it is more efficient.

Therefore, when considering whether to use a pointer type as a receiver, you still need to consider the actual situation.

When to Use Pointers #

From the detailed analysis of pointers above, we can summarize two advantages of pointers:

  1. They can modify the value to which they point.
  2. They can save memory when assigning variables and passing parameters.

Drawing 2.png

However, as a high-level language, Go language is still relatively restrained in the use of pointers. It introduces many restrictions on pointers during design, such as pointers cannot be calculated and cannot obtain pointers to constants. Therefore, when considering whether to use pointers, we should also maintain a restrained mindset.

Based on practical experience, I have summarized the following suggestions for using pointers for your reference:

  1. Do not use pointers for reference types such as map, slice, or channel.
  2. If you need to modify the internal data or status of a method receiver, you need to use a pointer.
  3. If you need to modify the value or internal data of a parameter, you also need to use a pointer type parameter.
  4. If it is a relatively large structure, each parameter passing or method invocation involves memory copying and consumes more memory. In this case, consider using a pointer.
  5. Small data types such as int and bool do not need to be used with pointers.
  6. If concurrency safety is required, try to avoid using pointers. If you have to use pointers, ensure concurrency safety.
  7. It is best not to nest pointers, that is, do not use a pointer to a pointer. Although Go allows this, it will make your code exceptionally complex.

Summary #

In order to make programming simpler, pointers have been gradually deemphasized in high-level languages. However, they do have their own advantages: modifying the value of data and saving memory. Therefore, in Go language development, we should use value types as much as possible, rather than pointer types, because value types can make development simpler and also ensure concurrency safety. If you want to use pointer types, refer to the conditions I mentioned above and see if they are met or necessary before using pointers.

This is the end of this lesson. At the end of this lesson, I would like to leave you with a question to think about: Does a pointer to an interface implement that interface? Why? After thinking about it, you can write your own code to verify it.