Forem

Cover image for Go Pointers vs. Values: When to Use Them (and When Not To)
Wycliffe A. Onyango
Wycliffe A. Onyango

Posted on

Go Pointers vs. Values: When to Use Them (and When Not To)

If you're new to Go, one thing you’ll quickly notice is that Go doesn’t have null like some other languages—it has zero values instead. But what if you need to tell the difference between a field that was never set and one that was explicitly set to zero? This is where pointers can help—but they can also introduce unnecessary complexity. Let’s break it down step by step.

The Problem: Missing vs. Zero Value

Imagine you’re working with JSON data that represents a user:

{ "name": "" }
Enter fullscreen mode Exit fullscreen mode

vs

{}
Enter fullscreen mode Exit fullscreen mode

If we define our Go struct like this:

type User struct {  
    Name string `json:"name"`  
}
Enter fullscreen mode Exit fullscreen mode

Both of the JSON examples above will result in user.Name == "" in Go. This means we can’t tell whether the name was:

  1. Intentionally set to an empty string.
  2. Never included in the JSON at all.

This can be a problem if missing values need special handling.

The Solution: Using Pointers for JSON

type User struct {  
    Name *string `json:"name"`  
}
Enter fullscreen mode Exit fullscreen mode

Now, we can check if Name is nil:

func main() {
    jsonData := `{}`  // No "name" field

    var user User
    json.Unmarshal([]byte(jsonData), &user)

    if user.Name == nil {
        fmt.Println("Name is missing")
    } else {
        fmt.Println("Name is:", *user.Name)
    }
}
Enter fullscreen mode Exit fullscreen mode

What Happens Here?

  • If the name field is absent in the JSON, user.Name == nil.

  • If the name field is present but empty ("name": ""), user.Name is not nil, but its value is "".

This is useful when working with APIs, databases, or configurations where a missing value is different from an empty one.

When Not to Use Pointers

Pointers can be overused, making code harder to read and more error-prone.

Example: Configuration Settings

Let’s say we need a configuration with a timeout value. A common mistake is using a pointer:

type Config struct {  
    Timeout *int  
}
Enter fullscreen mode Exit fullscreen mode

Now, every time we want to use it, we have to check if it’s nil:

if config.Timeout != nil {
    fmt.Println("Timeout is set to:", *config.Timeout)
} else {
    fmt.Println("No timeout set")
}
Enter fullscreen mode Exit fullscreen mode

This makes the code more complex than necessary. Instead, we can use a simple integer with a boolean flag:

type Config struct {  
    Timeout int  
    IsSet   bool  
}
Enter fullscreen mode Exit fullscreen mode

Now, we check if IsSet is true:

if config.IsSet {
    fmt.Println("Timeout is set to:", config.Timeout)
} else {
    fmt.Println("No timeout set")
}
Enter fullscreen mode Exit fullscreen mode

Why Is This Better?

  • No need for nil checks.
  • No extra memory allocations for pointers.
  • Simpler, more readable code.

Takeaways

  • Use pointers when you need to distinguish between "unset" and "zero value" (especially in JSON and APIs).

  • Use value types when a zero value is enough and you don’t need to track missing data separately.

  • Avoid unnecessary pointers to keep your Go code clean, efficient, and easy to maintain.

By following these simple rules, you’ll write better Go code with fewer headaches!

Top comments (0)