· 6 min read

Default Values and Zero Values in Golang

This article was auto-translated from Chinese. Some nuances may be lost in translation.

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

Explore Other Topics