When declaring variables in Go, we usually have two syntax options: In some scenarios, pointers; in others, reference; sometimes, either. It’s great to have choices, but it is also confusing sometimes as to which one in which scenario.
To be more reasonable in choice-making, I started from pointers, walked through their natures, and summarized some rules in using them in Go.
Pointers
Go has pointers. A pointer holds the memory address of a value.
— from A Tour of Go
The data is stored in the memory when the program runs and each has a number- the memory address, which can be assigned to a pointer, and through which we can find the data stored in memory.
A pointer variable, a variable that stores a pointer, can be converted with the real data in the memory address through the two operators of & and *.
func main() {
name := "slaise"
nameP := &name // get address
fmt.Println("name is:", name)
fmt.Println("name's address is':", nameP)
var p2 *string
p2 = &name
fmt.Println("value is: ", *p2)
}
The pointer operation is very basic, but there are several interesting points.
- The pointer type variable must be initialized with the address of a variable (&)or new. Otherwise, code like *p = “a” will get an error from the compiler.
var p *string
*p = "a" // error
p = new(string) // assign a real memory address for the pointer
*p = "a" // ok!
- Pointer values (*) can be superimposed, and **string is a reasonable pointer type. Of course, this pointer chain can continue endlessly, making the code readability a disaster, so almost nobody uses it this way in practice.
Pointers Dos and Don’ts
Dos
- Use the pointers when modifying the internal data or state of the method receiver.
- Use the pointer type parameter when modifying the parameter value or the internal data.
In modifying the input parameters, reference type variables can no longer work, and you must use pointers, such as the common swap method in algorithm testing.
- Use the pointer type field if you need to modify the field value inside a struct.
In this example, the field a of struct A cannot be modified by the method if it is not directly accessed viaa1.a, but can be done in subsequent methods(B.b) with pointers. Try it!
- Use slice over array’s pointer. Look at the example below, you’ll find that slice’s update method is much simpler and more readable, even if the two methods have the same effect.
- Another common application of pointers in structures is that the zero value of the dependent pointer is nil, which can be ignored during JSON parsing.
- The pointer type is needed when the struct is required as the slice and map value types and its variable needs direct modification later. Otherwise, you can only copy and create a new object to replace it, taking the possible performance risks into consideration.
In the above example, only when the value type is *A can you directly access the fields in A and modify them; Otherwise, a compilation error will occur. Try it!
- Consider using a pointer in a relatively large structure, where memory is copied every time a parameter is passed or a method is called, occupying a lot of memory. The most common application scenarios are in JSON parsing, reading the database table to a struct, cache updates, etc. Pointers can greatly improve efficiency when some structs reach more than 20 or even 100 fields.
Don’ts
- Do not use pointers for reference types like map, slice, channel.
This is easy to understand. We use pointers to directly obtain the variable address and then to modify the variable via the address, such as the above array. But as to map, slice, and channel, they are essentially implemented by pointers, and their elements can be directly modified with traversal access, such as the bucketfield in map.
- Avoid using pointers if concurrency safety is required. Keep in mind that it is an intrusive way to modify variables using pointers, and sometimes data safety is first priority.
- Try not to nest pointers like **string, which will make your code extremely complex, although it is allowed by Go.
Pointer for interfaces
In regard to pointer for interfaces, I think it deserves a separate section because of interface’s specialness: It is a collection of a set of operations, but also is a type like int and string. And the zero value of interface is nil. We can use interface a == interface b for comparison.
The interface is a very sophisticated topic if going into depth, so in this section, I will only talk about its implementation. As we all know, there are two implementing methods-One is using a pointer type of the struct, and the other is a reference type. See an example
type Shape interface {
draw()
}
type Circle struct{}
func (c *Circle) draw() {
fmt.Println("call from *Circle")
}
func (c Circle) draw() {
fmt.Println("call from Circle")
}
func main() {
var s Shape = &Circle{}
s.draw()
}
// code cannot compile, method redeclared: Circle.draw
Of course, the above code cannot compile because it contains two implementations of the same interface method.
To determine which one to apply, we need to know their difference, which can be seen easily by making a little modification to the above method.
type Shape interface {
draw()
}
type Circle struct{}
func (c *Circle) draw() {
fmt.Println("call from *Circle")
}
type Square struct{}
func (s Square) draw() {
fmt.Println("call from Square")
}
func main() {
var s1 Shape = &Circle{}
s1.draw()
// compile err cannot use Circle{} (type Circle) as type Shape in assignment:
// var s2 Shape = Circle{}
// s2.draw()
var s3 Shape = Square{}
s3.draw()
var s4 Shape = &Square{}
s4.draw()
}
As is shown, the pointer object must be used when initializing if we only use the pointer type to implement the interface. Otherwise, the compiler won’t recognize Circle as the implementation of Shape. However, Square can declare successfully in both ways since &Square itself is an implementation that can point to the real Sqaure.
Are there any other differences aside from that in initialization? Yes!
In Square, every call of the draw method is by pass-by-reference, copying a new Square object each time to execute the draw method. While, this value copy method is not functional when it comes to data updating and modifying, which can only be implemented with pointers. Let’s rewrite the above example by adding a field to Square and Circle. Try it.
type Shape interface {
draw()
}
type Circle struct {
a int
}
func (c *Circle) draw() {
(*c).a = 2
}
type Square struct {
b int
}
func (s Square) draw() {
s.b = 2
}
func main() {
c := &Circle{a: 1}
c.draw()
fmt.Printf("c.a = %v, \n", c.a)
s := Square{b: 1}
s.draw()
fmt.Printf("s.b = %d, \n", s.b) // still 1
}
// output
c.a = 2,
s.b = 1,
Pause before using pointers
Pointers are great, offering us a window to manipulate the data. Should we use it in all scenes? Absolutely, no!
The Immutable Pattern is “the why”.
In object-oriented programming, “immutable interface” is a pattern for designing an immutable object.[1] The immutable interface pattern involves defining a type which does not provide any methods which mutate state. Objects which are referenced by that type are not seen to have any mutable state, and appear immutable. — from wiki
And in today’s programming, people are more and more inclined to functional programming, no matter in Java, Go, front-end framework React, etc. Without any side effects, it turns out to be the most common programming paradigm in a distributed programming environment. An explanation in wiki,
In computer science, functional programming is a programming paradigm where programs are constructed by applying and composing functions.
— from wiki
Therefore, use pointers carefully, and consider whether the scenario supports deployment in the cloud and whether it is thread-safe.
The end
It is a summary of rules and my experience in using pointers, and the core is to determine when and where to use the modifiable pointers but not the immutable pass-by-value and pass-by-reference. Upon finishing this, I feel the urge to review my previous code to figure out whether my wrong usage of them had led to an increased security risk, or whether I misused them somewhere and caused more redundant copies.
I hope this article can help you with a better understanding of the pointers in Go.
Thanks for reading!
Note: The post is authorized by original author to republish on our site. Original author is Stefanie Lai who is currently a Spotify engineer and lives in Stockholm, original post is published here.