Default Values and Zero Values in Golang
In Golang, if a value is not assigned during initialization, the zero value is used instead.
However, after using it for a while, you’ll notice that always falling back to the zero value makes it hard to distinguish whether a user omitted the value or intentionally provided the zero value itself.
type User struct {
ID string
Email string
Name string
}
func main() {
user := &User{
ID: "1",
}
}
Since Email and Name are not provided here, they both default to "". So far, so good. But what if we want to serialize this into JSON?
package main
import (
"fmt"
"encoding/json"
)
type User struct {
ID string `json:"id"`
Email string `json:"email"`
Name string `json:"name"`
}
func main() {
user := &User{
ID: "1",
}
d, _ := json.Marshal(user)
fmt.Println(string(d))
}
This will output:
{
"id":"1",
"email":"",
"name":""
}
Quite often, our requirement is to represent email and name as null instead of "" when they have no value. How should we best handle this?
1. Switch to Pointers
We know that primitive types cannot have an initial value of nil, nor can you check them using expressions like if !str {...}—doing so results in mismatched types string and nil (a classic habit picked up from dynamic languages, sigh).
So, if a field needs to accept a nil value, you can consider using pointers, like this:
package main
import (
"fmt"
"encoding/json"
)
type User struct {
ID *string `json:"id"`
Email *string `json:"email"`
Name *string `json:"name"`
}
func ToStringPtr(str string) *string {
return &str
}
func main() {
user := &User{
ID: ToStringPtr("1"),
Email: ToStringPtr("kalan@gmail.com"),
}
d, _ := json.Marshal(user)
fmt.Println(string(d))
}
The output will now be:
{
"id":"1",
"email":"kalan@gmail.com",
"name":null
}
This looks much better. But is it really a good approach? Let’s take a closer look.
Because all fields are now pointers, accessing a value directly via user.Email does this:
package main
import (
"fmt"
)
type User struct {
ID *string `json:"id"`
Email *string `json:"email"`
Name *string `json:"name"`
}
func ToStringPtr(str string) *string {
return &str
}
func main() {
user := &User{
ID: ToStringPtr("1"),
Email: ToStringPtr("xxx@gmail.com"),
}
fmt.Println(user.Email) // 0x1040c130
val, _ := json.Marshal(user)
}
Direct access returns a pointer address. To retrieve the actual value, you have to dereference it with *user.Email. But doesn’t seeing *user.Email make you a bit nervous? For example, if we directly dereference *user.Name:
func main() {
user := &User{
ID: ToStringPtr("1"),
}
fmt.Println(*user.Name)
}
A panic is triggered immediately: panic: runtime error: invalid memory address or nil pointer dereference. Because the initial value is nil, it is a null pointer, and dereferencing a null pointer causes a panic. Therefore, while we can use nil to represent empty values, this approach clearly comes with significant downsides. We have to check whether the pointer is nil before using it.
Additionally, if this struct is intended for scanning values from an SQL database, you can pair it with sql.NullString or use the null package.
sql.NullString / sql.NullInt, etc.
If our defined struct is meant to map values from a database, it needs to implement an interface called Scanner. If a string is a null value, SQL cannot map it directly to a primitive string. sql.NullString implements the Scanner interface. If serialized directly to JSON, it looks like this:
{
"email": {
"Valid": true,
"String": "xxx@gmail.com"
}
}
You would need to implement MarshalJSON to convert it into the expected JSON format (though, technically, this structure is fine too…).
null
The null package defines common data types for you, making them easy to use with SQL or JSON.
type User struct {
ID null.String `json:"id"`
Email null.String `json:"email"`
Name null.String `json:"name"`
}
2. Customizing MarshalJSON
In Golang, we can define a custom type based on a primitive type and attach methods to it. We can override MarshalJSON so that what would normally output as a zero value serializes to null instead:
type nullString string
func (str nullString) MarshalJSON() ([]byte, error) {
raw := string(str)
if string(str) == "" {
return json.Marshal(nil)
}
val := raw
return json.Marshal(val)
}
func main() {
str := nullString("")
val, _ := json.Marshal(str)
fmt.Println(string(val))
}
3. Default Values + Tags
Golang doesn’t have the syntactic sugar found in higher-level languages, such as:
const getPerson = (id = 1, name = '') => (
)
def get_person(id=1, name='', is_admin=False):
...
Nor does it have a ternary operator to help determine default values. Consequently, implementing default values in Golang is a bit more cumbersome. Someone once asked about this on the mailing list, but the idea was dismissed. The reasoning was:
Why does this need additional, special support in the language? Go has … to support a variable number of arguments, from which you could figure out which type of call it means, and supply defaults for missing arguments. Or you could pass some out-of-range or zero value like nil or -1 or even 0 to required arguments to ask for a default value. This has a long tradition in C, so why are function-specific calling conventions not enough? I think I’d rather see myfunc(nil, nil, nil, nil, nil) than myfunc(, , , ,) to say “do what you do with all default values”, since while I’m developing a program it conflates whether I missed a parameter completely or meant to ask for a default value. This can cause silent errors in cases where the default value is no acceptable to the overall program. (As the code base grows we won’t always be able to control the defaults for code we use in others’ packages.) I also don’t see why it’s necessary to add a special token to represent “default value”, since we can accomplish this without adding more symbols to the language.
Essentially, the Go team doesn’t want you passing a bunch of func(nil, nil, nil, nil) just to trigger default values, or you can use variadic args... to inspect the count and types of arguments without needing language-level default values.
However, what if you genuinely need default values? What if you really don’t want fields to initialize to 0, "", etc.? In that case, you can try using Golang’s built-in struct tags, such as:
type People struct {
Name string `default:""`
Age int64 `default:10`
}
Using this approach can achieve the desired outcome, though implementing a fully featured tag processor involves handling quite a few edge cases. I’ll give implementing that a shot another day.
Conclusion
This article summarized the issues you might run into when dealing with default values and zero values in Golang, along with potential solutions.
Related Posts
- When a Measure Becomes a Target: From the Window Tax to Pull Request Counts I once wrote a script to tally how many PRs I contributed in a quarter, how many reviews I left, and how many tickets I closed, hoping to use numbers to prove my output to my manager. My manager simply remarked that performance isn't just about output. Years later, I finally understood—when a measure becomes a target, it ceases to be a good measure. From the British window tax and the Hanoi rat bounty to evaluating developers by PR counts today, the underlying mechanism is exactly the same.
- Using Cloudflare Images for Image Storage and Transformation Putting an image on a webpage is the simplest task in frontend development. But doing it properly—including resizing, generating multiple formats, and withstanding heavy traffic—is actually an entire end-to-end solution. Eventually, I offloaded everything to Cloudflare Images, keeping only a single original image.
- Stop Using AWS Access Keys Access Keys are an easily overlooked security risk in AWS. By pairing OIDC with IAM Roles, GitHub Actions can securely operate AWS resources without storing any secrets.
- Database Primary Keys: AUTO_INCREMENT, UUID, and UUIDv7 Backend developers often face the choice of primary keys: should you use auto-increment or UUID? What about collisions? How does UUIDv7 compare to created_at + index in performance? Here are the design decisions and benchmark results from testing 20 million rows.