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.
What is a pointer?
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.
When to use a pointer ?
1. When you want to modify the original value
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
}
2. When the value is large
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
}
3. When you want to avoid copying the value
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...
}
When not to use a pointer return?
1. When the value is a basic type
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
}
2. Return pointer can be confusing in some cases
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)
}
3. We don’t need modify the original value
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.
Handling Empty Check Data Without Pointers
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
Conclusion
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.