Practical Go: Using return pointer or not?

• 📚 5 min read • Tweet this post

When it comes to Go programming, there are situations where you need to decide whether to return a pointer or not. In this article, we’ll explore when to use a pointer return and when not to. We’ll also discuss how to handle empty data checks when not using pointers.

A pointer is a variable that stores the memory address of another variable. In Go, you can declare a pointer by using the * symbol before the type, such as *int for a pointer to an integer. You can use the & operator to get the address of a variable, and the * operator to access the value stored in the memory location pointed to by the pointer. You can read the previous post about heap and stack to understand the different of memory location if using pointer or not.

If you want to modify the original value of a variable, you should return a pointer to that variable. When you pass a variable to a function, a copy of that variable is created. If you modify the copy, the original variable remains unchanged. By returning a pointer to the original variable, you can modify its value from within the function.

func modifyValue(x *int) {
    *x = 10
}

func main() {
    x := 5
    modifyValue(&x)
    fmt.Println(x) // Output: 10
}

If you’re working with large data structures, like a big array or a struct, it’s more efficient to pass a pointer to the data than to make a copy of it. This can improve the performance of your program, especially if you’re working with a lot of data.

type LargeStruct struct {
    Data [1000000]int
}

func modifyStruct(s *LargeStruct) {
    s.Data[0] = 10
}

func main() {
    s := LargeStruct{}
    modifyStruct(&s)
    fmt.Println(s.Data[0]) // Output: 10
}

When you return a pointer to a value, you’re avoiding the cost of copying the value. If you’re working with large data, this can be a significant performance gain. Additionally, if the value you’re working with is expensive to copy, like a mutex or a file, returning a pointer can be a good option.

func expensiveOperation() *sync.Mutex {
    return &sync.Mutex{}
}

func main() {
    m := expensiveOperation()
    m.Lock()
    defer m.Unlock()
    // Do some work...
}

If you’re working with basic types like int, float, bool, or string, it’s usually not necessary to return a pointer. These types are small and cheap to copy, so you won’t see much of a performance gain by using a pointer. Additionally, using pointers with basic types can make your code more complicated.

func square(x int) int {
    return x * x
}

func main() {
    x := 5
    y := square(x)
    fmt.Println(y) // Output: 25
}

In some cases, returning a pointer can cause confusion and make the code harder to understand. If you do not need to modify the value of a variable in the calling function, you can simply return the value itself. This makes the code clearer and easier to read.

For example, consider the following function that returns the length of a string:

func lenStr(s string) *int {
  length := len(s)
  return &length
}

This function returns a pointer to an integer containing the length of the string. However, it can be rewritten as follows to return the length of the string directly:

func lenStr(s string) int {
  return len(s)
}

The opposites with the reason why using pointer, this reason is usually when we fetch data from storage provider, such as get from database or any other resource. We only need to fetch the data and no need to update original values. If the data fetching is massive and data size is relatively small, then return data with pointer can burden the heap, it’s better to return non pointer struct.

When working with data that may be empty or nil, you can use the zero value of the data type to handle the situation. The zero value of a data type is the value that is assigned to a variable if no value is provided. For example, the zero value of an integer is 0, and the zero value of a string is an empty string ("").

But if the data type is struct we can use approach generic nullable values. The concept is simple, the struct have Value T and valid bool variable, if we need nullable or invalid value we just passing empty struct with false empty := Nullable(User{}, false). Using this approach can simplify checking invalid as nil values.

package main

import "fmt"

type Null[T any] struct {
	Value T
	valid bool
}

// not using the type param in this method
func (n Null[_]) IsValid() bool {
	return n.valid
}

func Nullable[T any](value T, valid bool) Null[T] {
	c := Null[T]{Value: value, valid: valid}
	return c
}

func Example_nullableInt() {
	// A third way would be to use the `fmt.Sprintf` method:
	nullInt := Nullable(1, true)
	fmt.Println(nullInt.Value)
	// Output:
	// 1
}

type User struct {
	Name    string
	Address string
}

func Example_nullableStructEmpty() {
	// A third way would be to use the `fmt.Sprintf` method:
	empty := Nullable(User{}, false)
	fmt.Println(empty.IsValid(), empty.Value.Name)
	// Output:
	// false
}

func Example_nullableStruct() {
	// A third way would be to use the `fmt.Sprintf` method:
	user := Nullable(User{Name: "Lutfi", Address: "depan komputer"}, true)
	fmt.Println(user.IsValid(), user.Value.Name)
	// Output:
	// true Lutfi
}

Try it yourself https://go.dev/play/p/t5PlI6ho1rG

The use of pointers in Go should be considered based on the specific needs of the program. When returning large data structures or modifying values in the calling function, pointers can be useful. However, returning pointers unnecessarily can make the code harder to understand. Additionally, handling empty check data without pointers can be achieved by using the zero value of the data type.

programming go practical pointer