Basics
- A pointer is simply a variable that holds the location in memory where a value is stored.
- The zero value for a pointer is nil
- Go's pointer syntax is partially borrowed from C and C++. However some of the tricks, including pointer arithmetic, are not allowed in Go.
- Since Go has a garbage collector, most of the pain of memory management is removed.
- The & is the address operator. It precedes a value type and returns the address of the memory location where the value is stored
- The * is the indirection operator. It precedes a variable of pointer type and returns the pointed-to value. This is called dereferencing
- A pointer type is a type that represents a pointer. It is written with a * before a type name. A pointer type can be based on any type
- The built-in function new creates a pointer variable. It returns a pointer to a zero value instance of the provided type
- The lack of immutable declarations in Go might seem problematic, but the ability to choose between value and pointer parameter types addresses the issue.
- Rather than declare that some variables and parameters are immutable, Go developers use pointers to indicate that a parameter is mutable.
/* empty pointer */
var pointerA *string
fmt.Println(pointerA == nil) // true
fmt.Println(*pointerA) // panics
/* pointer with a data type */
var x int32 = 10
pointerB := &x // referencing
fmt.Println(*pointerB) // 10 - dereferencing
/* pointer arithmetic */
z := 5 + *pointerB
fmt.Println(z) // 15
/* pointer variable using "new" keyword */
var pointerC = new(int)
fmt.Println(pointerC == nil) // false
fmt.Println(*pointerC) // 0
/* pointer to struct */
x := &Foo{}
- Before dereferencing a pointer, you must make sure that the pointer is non-nil. Your program will panic if you attempt to dereference a nil pointer
Passing pointers in functions
Passing indicate Mutable Parameters
- Since Go is a call by value language, the values passed to functions are copies. For nonpointer types like primitives, structs, and arrays, this means that the called function cannot modify the original. Since the called function has a copy of the original data, the immutability of the original data is guaranteed.
- However, if a pointer is passed to a function, the function gets a copy of the pointer. This still points to the original data, which means that the original data can be modified by the called function.
func failedUpdate(px *int) {
x2 := 20
px = &x2
}
func update(px *int) {
*px = 20
}
func main() {
x := 10
failedUpdate(&x)
fmt.Println(x) // 10
update(&x)
fmt.Println(x) // 20
}
Pointers are a last resort
As pointers make it harder to understand data flow and can create extra work for the garbage collector. Use them only when necessary- Rather than populating a struct by passing a pointer to it into a function, have the function instantiate and return the struct
- The only time you should use pointer parameters to modify a variable is when the function expects an interface
- When returning values from a function, you should favor value types. Only use a pointer type as a return type if there is state within the data type that needs to be modified.
/* not this */
func MakeFoo(f *Foo) error {
f.Field1 = "val"
f.Field2 = 20
return nil
}
/* but this */
func MakeFoo() (Foo, error) {
f := Foo{
Field1: "val",
Field2: 20,
}
return f, nil
}
Pointers-passing performance
- If a struct is large enough, there are performance improvements from using a pointer to the struct as either an input parameter or a return value. The time to pass a pointer into a function is constant for all data sizes, as the size of a pointer is same for all data types.
-
The behavior for returning a pointer versus returning a value is
more interesting.Source
- For data structures that are smaller than a megabyte, it is actually slower to return a pointer type than a value type.
- Once your data structures are larger than a megabyte, the performance advantage flips.
Zero value v/s no value
-
If this distinction matters in your program, use a nil pointer to
represent an unassigned variable or struct field.
- Because pointers also indicate mutability, be careful when using this pattern.
- Rather than return a pointer set to nil from a function, use the comma ok idiom, similar to maps.
- if a nil pointer is passed into a function via a parameter or a field on a parameter, you cannot set the value within the function as there's nowhere to store the value.
- While a pointer does provide a handy way to indicate no value, if you are not going to modify the value, you should use a value type instead, paired with a boolean.
Maps v/s Slices
Maps
-
Any modifications made to a map that's passed to a function are
reflected in the original variable that was passed in. Why?
- within the Go runtime, a map is implemented as a pointer to a struct. Passing a map to a function means that you are copying a pointer.
- Because of this, you should avoid using maps for input parameters or return values, especially on public APIs.
-
On an API-design level, maps are a bad choice. Why?
- they say nothing about what values are contained within
- there's nothing that explicitly defines what keys are in the map, so the only way to know what they are is to trace through the code
-
From the standpoint of immutability, maps are bad. Why?
- the only way to know what ended up in the map is to trace through all of the functions that interact with it.
- This prevents the API from being self-documenting.
- Go is a strongly typed language; rather than passing a map around, use a struct.
Slices
-
Modification to the contents of the slice is reflected in the
original variable
-
but using append to change the length isn't reflected in the
original variable, even if the slice has a capacity greater than
its length
- That's because a slice is implemented as a struct with three fields: an int field for length, an int field for capacity, and a pointer to a block of memory
- When a slice is copied to a different variable or passed to a function, a copy is made of the length, capacity, and the pointer.
- Changing the values in the slice changes the memory that the pointer points to, so the changes are seen in both the copy and the original.
- Changes to the length and capacity are not reflected back in the original, because they are only in the copy. Changing the capacity means that the pointer is now pointing to a new, bigger block of memory.
- In case when capacity is greater than length (even after append), the elements are appended in the same location. But as the length of the original slice remains same, it's not able to access those elements.
-
but using append to change the length isn't reflected in the
original variable, even if the slice has a capacity greater than
its length
- The reason you can pass a slice of any size to a function is that the data that's passed to the function is the same for any size slice: two int values and a pointer.
- The reason that you can't write a function that takes an array of any size is because the entire array is passed to the function, not just a pointer to the data.
Offloading Garbage Collection
Comparison with Java
- local variables and parameters are stored in the stack, just like Go
- Howeve, objects in Java are implemented as pointers - every object variable instance, only the pointer to it is allocated on the stack; the data within the object is allocated on the heap.
- Only primitive values (numbers, booleans, and chars) are stored entirely on the stack.
- things like Lists in Java are actually a pointer to an array of pointers. Even though it looks like a linear data structure, reading it actually involves bouncing through memory, which is highly inefficient.
- To work around all of this inefficiency, the Java Virtual Machine includes some very clever garbage collectors that do lots of work, some optimized for throughput, some for latency, and all with configuration settings to tune them for the best performance.
Go encourages using pointers sparingly.
- The above-mentioned workload on the Java (or similar languages) Garbage collector is why, Go encourages developers to use pointers sparingly.
- We reduce the workload of the garbage collector by making sure that as much as possible is stored on the stack.
- Slices of structs or primitive types have their data lined up sequentially in memory for rapid access. And when the garbage collector does do work, it is optimized to return quickly rather than gather the most garbage.