What is a Pointer?

In the Go language, a pointer is a variable that stores the memory address of another variable. Therefore, a pointer variable points to the memory address of another variable, not the variable itself.

When declaring a pointer variable, you need to add * before the variable name, indicating that this is a pointer variable, for example:

1
var p *int

This indicates that a pointer named p has been declared, which points to an integer variable. The & operator can be used to get the address of a variable, for example:

1
2
x := 10
p := &x

This means getting the address of the variable x and assigning it to the pointer variable p.

When using a pointer to access a variable, you need to use the * operator, which is the dereference operator, for example:

1
2
3
x := 10
p := &x
fmt.Println(*p)

This means printing the value of the variable pointed to by the pointer variable p, which is the value of the variable x.

In addition to declaring pointer variables and obtaining the addresses of variables, you can also use the new function to create pointer variables. For example, creating a pointer to an integer variable:

1
p := new(int)

This will create a new integer variable, return its address, and then assign the address to the pointer variable p.

Pointers can also be used for function parameters and return values to share data between function calls.

When using pointers, be careful because if a pointer points to an invalid memory address, the program may crash or exhibit unpredictable behavior. Therefore, when using pointers, ensure that the memory address pointed to by the pointer is valid.

Use Cases for Pointers

Passing Parameters via Pointer Functions

Pointers can be used to pass parameters to functions. When a function needs to modify the value of an actual parameter, the address of the actual parameter can be passed as a formal parameter to the function, and the value of the actual parameter can be modified through the pointer. For example:

1
2
3
4
5
6
7
8
9
func modify(s *string) {
    *s = "hello, world"
}

func main() {
    s := "hello"
    modify(&s)
    fmt.Println(s) // Output: hello, world
}

Accessing Struct Fields via Pointers

Pointers can more conveniently access fields in a struct, especially when the struct is large, passing a pointer is more efficient than passing the entire struct. For example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
type Person struct {
    Name string
    Age  int
}

func main() {
    p := &Person{
        Name: "Alice",
        Age:  18,
    }
    p.Name = "Bob"
    fmt.Println(p) // Output: &{Bob 18}
}

Dynamically Allocating Memory via Pointers

Pointers can dynamically allocate memory at runtime, such as using the new() function to create a new variable and return its address. For example:

1
2
3
4
5
func main() {
    p := new(int)
    *p = 42
    fmt.Println(*p) // Output: 42
}

Passing Variadic Arguments

In Golang, you can use pointers to pass variadic arguments. This is because when using variadic arguments, what is passed is a slice (slice), and a slice is essentially a pointer to an array. This operation can avoid copying large amounts of data.

For example, consider the following function, which accepts variadic arguments and prints them out:

1
2
3
4
5
func printArgs(args ...int) {
    for _, arg := range args {
        fmt.Println(arg)
    }
}

Now, if you want to pass a slice as an argument to another function, you can use a pointer:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
func printArgsPtr(args *[]int) {
    for _, arg := range *args {
        fmt.Println(arg)
    }
}

func main() {
    args := []int{1, 2, 3}
    printArgsPtr(&args)
}

In this example, we define a new function printArgsPtr that uses a pointer as a parameter to receive a slice. Inside the function, we use *args to dereference the pointer and get the actual slice, and then iterate over it to print each element. In the main function, we create a slice and pass its address to the printArgsPtr function.

It’s important to note that when using pointers to pass variadic arguments, if the slice is empty, you should not pass a nil pointer, but rather an empty slice. This is because in Golang, an empty slice and a nil pointer have different meanings, which I will explain later.

Besides the above aspects, pointers are also very useful in some specific scenarios, such as handling data structures, where pointers can more efficiently manipulate linked lists, trees, and other data structures. However, it’s important to note that overusing pointers can make code difficult to maintain, so they should be used with caution.

Common Nil Data Types

In the Go language, there are several nil data types, including:

  • Nil pointer (nil pointer): A pointer that points to a non-existent address, generally represented by nil.
  • Nil slice (nil slice): A slice with a length of 0, generally represented by nil.
  • Nil map (nil map): A map with a length of 0, generally represented by nil.
  • Nil interface (nil interface): An interface with both type and value as nil, generally represented by nil.
  • Nil channel (nil channel): A channel that has not been allocated storage space, generally represented by nil.

nil represents a null pointer, i.e., a pointer variable that does not point to any address, and can represent the zero value of pointer, interface, map, channel, and function types, but cannot be used to represent the zero value of other types such as int, float, bool, etc.

These nil data types are defined as the zero values of their respective types, where the zero values of pointers, slices, maps, interfaces, and channels are all nil, indicating that the default values of these types are nil, i.e., they do not point to any actual data. Therefore, when using these data types, they can be initialized to their zero values, or explicitly assigned to nil.

For example:

1
2
3
4
5
var s []int = nil // Define a nil slice
var p *int = nil  // Define a nil pointer
var m map[string]int = nil // Define a nil map
var i interface{} = nil // Define a nil interface
var c chan int = nil // Define a nil channel

Note the following points:

  • nil is not a keyword, but a predefined identifier. In Go, nil can be assigned to any type of pointer, slice, map, function, and channel variables.
  • Nil slices and nil maps need to be initialized before use, otherwise, a panic exception will occur.
  • Nil channels need to be initialized with the make function before they can be used.
  • Nil pointers can be used directly because their value is nil.
  • Nil interfaces can be used directly without initialization. Because a nil interface is essentially a variable of type interface{}, which can accept any type of value, including nil. So, when using a nil interface, if no explicit value is assigned, it is a nil interface and can be used directly.

Avoiding Nil Pointer Exceptions

When using pointer-type variables, perform a nil check to avoid nil pointer exceptions.

For example:

Integer Pointer Type

1
2
var p *int
fmt.Println(p == nil) // Outputs true

In the code, p is a variable of integer pointer type, and since it has not been initialized, its value is nil. You can check if it is a nil pointer by comparing p to nil.

Integer Slice Type

1
2
var s []int
fmt.Println(s == nil) // Outputs true

In the code, s is a variable of integer slice type, and since it has not been initialized, its value is nil. You can check if it is a nil slice by comparing s to nil.

Map Type

1
2
var m map[string]int
fmt.Println(m == nil) // Outputs true

In the code, m is a variable of map type, and since it has not been initialized, its value is nil. You can check if it is a nil map by comparing m to nil.

Function Type

1
2
var f func(int) int
fmt.Println(f == nil) // Outputs true

In the code, f is a variable of function type, and since it has not been initialized, its value is nil. You can check if it is a nil function by comparing f to nil.

Integer Channel Type

1
2
var ch chan int
fmt.Println(ch == nil) // Outputs true

In the code, ch is a variable of integer channel type, and since it has not been initialized, its value is nil. You can check if it is a nil channel by comparing ch to nil.

Finally, it is particularly important to note that empty arrays, empty slices, and empty maps can use the len() function to get their length, and the result will be 0. However, nil pointers, nil interfaces, and nil channels cannot use the len() function to get their length, otherwise, a runtime error will occur.

This is because, in Go, the len() function returns the length of a value, which is pre-defined and usually calculated by the compiler. The length of empty slices, empty arrays, and empty maps is 0 because they already have a block of memory, so the len() function can be used to get their length.

For nil interfaces and nil channels, which do not store data, their length is meaningless, so using the len() function will cause a runtime error. If you need to determine whether they are nil, you should use the nil value for the check.