Pointers You Should Know in Golang

Freedom is what you do with what’s been done to you.
— Jean-Paul Sartre

Medium Link: Pointers You Should Know in Golang | by Wesley Wei | Jul, 2024 | Programmer’s Career

Hello, here is Wesley, Today’s article is about Pointers in Go. Without further ado, let’s get started.💪

1.1 Pointer Basics

Introduce what a pointer is, how it works in memory, and why we need pointers.

In the Go language, if we want to store an integer, we use the int type, and if we want to store a string, we use the string type. But how do we store a memory address? What data type should we use?

The answer is a pointer.

When we declare a variable, the computer allocates a block of memory for it and stores the data in that block. A pointer is a variable that stores the memory address of another variable. Through a pointer, we can indirectly access or modify the value stored at that memory address.

For example:

1
2
var a int = 42
var p *int = &a // p is a pointer that points to variable a's memory address

In this code, a is an integer variable, and p is a pointer that points to a. The &a operator represents the memory address of a, and p stores that address.

image.png

As shown in the diagram, a pointer type variable itself has its own memory address.

We all know that using pointers has several benefits, including:

  1. Efficient data transfer: When calling a function, passing a pointer instead of the entire variable can avoid copying data and improve program performance.
  2. Sharing data: Multiple functions or data structures can share the same data by operating on the same memory address through pointers, achieving data sharing and consistency.
  3. Dynamic memory management: Through pointers, we can dynamically allocate, access, and release memory to meet more complex memory operation requirements.

Of course, Go’s pointers also have their own special features, which we will continue to explore below.

1.2 Pointer Declaration and Usage:

Learn how to declare and initialize pointers in the Go language, and understand how to use them.

In the Go language, declaring and initializing pointers is very simple. You can use the * symbol to declare a pointer type and the & symbol to get the address of a variable and initialize a pointer.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import "fmt"

func main() {
var a int = 42
var p *int = &a // p is a pointer that points to an int type, pointing to variable a's address

fmt.Println("a's value:", a) // Output: a's value: 42
fmt.Println("p points to:", *p) // Output: p points to: 42

*p = 21 // Modify the value of variable a through the pointer
fmt.Println("After modification, a's value:", a) // Output: After modification, a's value: 21
}

In this example, we first declare an integer variable a, then declare a pointer that points to a. Through *p, we can access and modify the value of a.

In Go, the zero value of a pointer is nil, indicating that the pointer does not point to any valid address. Before using a pointer, it’s usually necessary to check if the pointer is nil to avoid runtime errors.

1
2
3
4
5
6
7
8
var p *int
fmt.Println(p) // Output: <nil>

if p != nil {
fmt.Println(*p)
} else {
fmt.Println("p is nil")
}

More information about nil: Nil Notions You Should Know in Golang | by Wesley Wei | Jun, 2024 | Programmer’s Career

1.3 No Support for Pointer Operations

Go language does not support direct pointer operations.

In C language, you can freely use pointers (Pointer) to operate memory, and C language supports pointer operations, which can develop high-performance programs, but it also easily causes memory leaks and overflows.

Many syntax and programming ideas in the Go language come from the C language, but the Go language emphasizes safety and simplicity. To avoid programming errors and complexity, the Go language does not support direct pointer operations. For example, you cannot increment or decrement a pointer to traverse memory addresses.

1
2
3
4
5
6
var a int = 42
var p *int = &a

p++ // invalid operation: p++ (non-numeric type *int)
p-- // invalid operation: p-- (non-numeric type *int)
p = p + 1 // invalid operation: p + 1 (mismatched types *int and int)

In summary, to enjoy the benefits of pointers while avoiding their dangers, Go language restricts pointer operations as follows:

  1. Pointers cannot perform mathematical operations.
  2. Pointers of different types cannot be converted to each other.
  3. Pointers of different types cannot be compared.
  4. Pointers of different types cannot be assigned to each other.

However, in some scenarios, using non-safe pointers is more convenient and efficient, so the unsafe package provides an unsafe.Pointer type, which can be referred to in section 1.9.

1.4 Pointer Parameters in Functions:

Learn how to use pointer parameters in functions to modify external variable values within the function.

In Go language, function parameters are passed by value. This means that when a variable is passed to a function, the function receives a copy of the variable, rather than the original variable itself. Modifying the copy will not affect the original variable.

If you want to modify an external variable’s value within a function, you can pass a pointer to achieve this.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func safeUpdate(ptr *int) {
if ptr != nil {
*ptr = 100
} else {
fmt.Println("it is nil")
}
}

func main() {
var a int = 42
safeUpdate(&a) // normal update value
fmt.Println(a) // 100
safeUpdate(nil) // output: it is nil
}

The advantages of pointer parameters:

  1. Reduce memory copying: Passing pointers can avoid copying entire data structures (such as arrays, slices, and structs), improving performance.
  2. Implement reference passing: Through pointer parameters, functions can directly modify external variable values without just modifying their copies.
  3. Consistency: Multiple functions can share and modify the same variable through pointer parameters, maintaining data consistency.

1.5 Pointers and Arrays:

Learn how to combine pointers with arrays.

  • Arrays: In Go, an array is a fixed-length sequence of elements with the same type.
  • Pointers pointing to array elements: A pointer can point to an element in an array, allowing indirect access and modification of the array’s elements.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import "fmt"

func main() {
var arr [5]int = [5]int{10, 20, 30, 40, 50}
var p *int = &arr[0] // pointer p points to the first element of the array

// access and modify array elements through the pointer
fmt.Println(*p) // output: 10
*p = 100
fmt.Println(arr) // output: [100 20 30 40 50]

// move the pointer to the next element in the array
p = &arr[1]
fmt.Println(*p) // output: 20

*p = 200
fmt.Println(arr) // output: [100 200 30 40 50]
}
  • Array length is fixed: The length of an array is determined at declaration time and cannot be changed dynamically. When operating on arrays using pointers, ensure that the pointer does not go out of bounds.
  • Pointer arithmetic limitations: Go does not support direct pointer arithmetic, so it is not possible to traverse an array using p++ or p--. Instead, explicitly operate on the pointer to access different elements.
  • Slice alternatives: In many cases, slices can provide more flexible and convenient operations. A slice is a dynamic view of an array that allows for easier traversal and modification.

1.6 Pointers and Slices:

Understand the underlying implementation of slices and their relationship with pointers, and learn how to use pointers to operate on slices.

  • Slices: A slice is a reference to a contiguous segment of an array. A slice consists of three parts: a pointer (pointing to the start of the underlying array), length (the number of elements in the slice), and capacity (the number of elements from the start to the end of the underlying array).
  • Underlying array: A slice relies on an underlying array. All operations on the slice are actually operations on this underlying array.
1
2
3
4
5
type SliceHeader struct {
Data uintptr // pointer to the underlying array
Len int // length of the slice
Cap int // capacity of the slice
}
  • Through pointers, you can operate on the elements of a slice. You can get the address of an element in the slice and modify its value.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main

import "fmt"

func main() {
// declare and initialize an array
arr := [5]int{10, 20, 30, 40, 50}

// create a slice that references the entire array
slice := arr[:]

// get the pointer to the first element of the slice
p := &slice[0]

// access and modify elements in the slice through the pointer
fmt.Println(*p) // output: 10
*p = 100
fmt.Println(slice) // output: [100 20 30 40 50]

// access and modify other elements in the slice
p = &slice[1]
fmt.Println(*p) // output: 20
*p = 200
fmt.Println(slice) // output: [100 200 30 40 50]
}

Slice considerations:

  • Slices share underlying arrays: Multiple slices can share the same underlying array, and modifying one slice’s elements will affect other slices that share the same array.
  • Avoid out-of-bounds access: When operating on slices using pointers, ensure that the pointer does not go out of bounds.
  • Performance impact of slice growth: Frequent slice growth can lead to multiple memory allocations and data copies, which should be avoided.

1.7 Pointers and Structs:

Learn how to use pointers to operate on structs, as well as pointer receivers in struct methods.

  • Structs: A struct is a composite data type that combines multiple fields of different types into a single unit.
  • Pointers to structs: Pointers can point to structs, allowing access and modification of the struct’s fields through the pointer.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import "fmt"

// Define a struct type
type Person struct {
Name string
Age int
}

func main() {
// Create a struct instance
person := Person{Name: "Alice", Age: 30}

// Create a pointer to the struct
p := &person

// Access and modify the struct's fields through the pointer
fmt.Println(p.Name) // Output: Alice
fmt.Println((*p).Name) // Output: Alice
p.Age = 31
fmt.Println((*p).Age) // Output: 31
}

As you can see, p.Name is equivalent to (*p).Name.

Pointer Receivers

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import "fmt"

// Define a struct type
type Person struct {
Name string
Age int
}

// Define a method with a pointer receiver
func (p *Person) CelebrateBirthday() {
p.Age++
}

func main() {
// Create a struct instance
person := Person{Name: "Alice", Age: 30}

// Call the method to modify the struct's field
person.CelebrateBirthday()
fmt.Println(person.Age) // Output: 31
}
  1. Value Receivers vs Pointer Receivers: If a method does not need to modify the struct’s fields, it can use value receivers; if it needs to modify them, it should use pointer receivers.
  2. Accessing Struct Fields: Pointers can be used to conveniently access and modify struct fields, but care must be taken to avoid accessing null pointers.

1.8 Memory Allocation:

Deeply understand the usage scenarios and differences between new and make.

new and make Differences

  1. new: new is a built-in function that allocates memory. It returns a pointer to the type’s zero value. new(T) allocates memory for type T, initializes it with the type’s zero value, and returns a pointer of type *T.
  2. make: make is also a built-in function, but it only creates and initializes slices, maps, and channels. make returns an initialized (non-zero) value, not a pointer.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import "fmt"

// Define a struct type
type Person struct {
Name string
Age int
}

func main() {
// Use new to allocate a struct
p := new(Person)
fmt.Println(p) // Output: &{0}

// Modify the struct's fields
p.Name = "Alice"
p.Age = 30
fmt.Println(p) // Output: &{Alice 30}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import "fmt"

func main() {
// Use make to create a slice
slice := make([]int, 5) // Create a slice with length and capacity both equal to 5
fmt.Println(slice) // Output: [0 0 0 0 0]

// Use make to create a map
m := make(map[string]int)
m["key1"] = 42
fmt.Println(m) // Output: map[key1:42]

// Use make to create a channel
ch := make(chan int, 2)
ch <- 1
ch <- 2
fmt.Println(<-ch) // Output: 1
fmt.Println(<-ch) // Output: 2
}

new is used to allocate memory and return a pointer, while make is used to create and initialize slices, maps, and channels. In actual development, choose the appropriate memory allocation method based on your needs to improve code performance and maintainability.

1.9 unsafe package:

The unsafe package provides some operations that bypass Go’s type safety, allowing for low-level memory operations. Although using the unsafe package can achieve high-level functionality, it may also lead to program crashes or insecurity, so use with caution.

unsafe package - unsafe - Go Packages

The unsafe package provides an unsafe.Pointer type essence with a *int, which can point to any type:

1
2
3
4
5
6
7
type ArbitraryType int

type Pointer *ArbitraryType

func Sizeof(x ArbitraryType) uintptr
func Offsetof(x ArbitraryType) uintptr
func Alignof(x ArbitraryType) uintptr

The unsafe package provides three functions:

  1. func Sizeof(x ArbitraryType) uintptr: returns the size of x in bytes, excluding the size of the content pointed to by x.
  2. func Offsetof(x ArbitraryType) uintptr: returns the offset of a struct member from the start of the struct, passed as an argument.
  3. func Alignof(x ArbitraryType) uintptr: returns the alignment requirement for the type of x.

Note that all return types are uintptr. uintptr is an integer type that is large enough to hold the bit pattern of any pointer. However, it does not have pointer semantics and will be garbage-collected.

1
2
3
// uintptr is an integer type that is large enough to hold the bit pattern of
// any pointer.
type uintptr uintptr

uintptr definition

The unsafe.Pointer type cannot perform arithmetic operations directly, but can be converted to uintptr, performed arithmetic operations on uintptr, and then converted back to a pointer.

Here is an example of using unsafe.Pointer:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package main

import (
"fmt"
"unsafe"
)

func main() {
// Create an integer array
arr := [5]int{10, 20, 30, 40, 50}

// Get the pointer to the first element of the array
p := unsafe.Pointer(&arr[0])

// Print the original array and the value of the first element
fmt.Println("Original array:", arr)
fmt.Println("First element:", *(*int)(p))

// Calculate the pointer to the next element
sizeOfInt := unsafe.Sizeof(arr[0])
p = unsafe.Pointer(uintptr(p) + sizeOfInt)

// Print the value of the second element
fmt.Println("Second element:", *(*int)(p))

// Continue calculating and printing the values of subsequent elements

// To demonstrate garbage collection issues, create a pointer and set it to nil
ptr := &arr[0]
fmt.Println("Pointer before:", *ptr)
ptr = nil
fmt.Println("Pointer after nil:", ptr)

// Even though the pointer is set to nil, the array still exists because it is within the scope of the main function
fmt.Println("Array after nil pointer:", arr)
}

You can see the difference between uintptr and unsafe.Pointer:

  • unsafe.Pointer is a general-purpose pointer type used for converting different types of pointers. It cannot participate in pointer arithmetic.
  • uintptr is used for pointer arithmetic, but does not have pointer semantics and will be garbage-collected.
  • unsafe.Pointer can be converted to and from uintptr.
  • However, it is not recommended to use unsafe.Pointer without careful consideration, as it may lead to memory truncation or access extension issues.

Here is an example of using unsafe.Pointer incorrectly:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import (
"fmt"
"unsafe"
)

func main() {
var a int = 42
var p *int = &a

// Convert *int to unsafe.Pointer
var uptr unsafe.Pointer = unsafe.Pointer(p)

// Convert unsafe.Pointer to *float32
var fptr *float32 = (*float32)(uptr)

fmt.Println(*fptr) // Not safe, output is unpredictable
}

This code is not safe and may produce unpredictable results.


More Series Articles about You Should Know In Golang:

https://wesley-wei.medium.com/list/you-should-know-in-golang-e9491363cd9a

And I’m Wesley, delighted to share knowledge from the world of programming. 

Don’t forget to follow me for more informative content, or feel free to share this with others who may also find it beneficial. it would be a great help to me.

See you in the next article. 👋

中文文章: https://programmerscareer.com/zh-cn/golang-pointers/
Author: Wesley Wei – Twitter Wesley Wei – Medium
Note: Originally written at https://programmerscareer.com/golang-pointers/ at 2024-07-04 01:03. If you choose to repost or use this article, please cite the original source.

Timer changes in Go 1.23 Welcome to the AI Revolution: My Guide to Essential Tools and Concepts

Comments

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×