Kalan's Blog

Software Engineer / Taiwanese / Life in Fukuoka

Current Theme light

In golang, if a variable is not assigned a value during initialization, it will use the zero value.

However, after using it for a while, we may find it difficult to distinguish whether the zero value is due to the user not inputting a value or if the user intentionally inputted the zero value.

type User struct {
    ID    string
    Email string
    Name  string
}

func main() {
    user := &User{
        ID: "1",
    }
}

In this case, since Email and Name are not assigned values, they will default to "". So far, everything seems fine. But what if we want to serialize it to 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))
}

The output will be:

{
    "id":"1",
    "email":"",
    "name":""
}

Our requirement is often to represent email and name as null instead of "" when they have no value. How can we modify it?

1. Use pointers

We know that the initial value of a primitive type cannot be nil, and we cannot use operations like if !str {...} to check, as it will result in a mismatched types string and nil error (coming from being accustomed to dynamically typed languages).

So, if a field needs to accept a nil value, we can consider using pointers, for example:

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))
}

Now the output will be:

{
    "id":"1",
    "email":"kalan@gmail.com",
    "name":null
}

This looks much better, but is it really a good solution? Let's continue.

Since all the fields have become pointers, if we directly use user.Email to get the value:

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)
}

If we directly get the value, it will return the memory address, and we need to rewrite it as *user.Email to get the value. But doesn't *user.Email look a bit shaky? For example, if we directly use *user.Name to get the value:

func main() {
    user := &User{
        ID: ToStringPtr("1"),
    }
    fmt.Println(*user.Name)
}

This will cause a panic: panic: runtime error: invalid memory address or nil pointer dereference because the initial value is nil, indicating it is a null pointer, and accessing a null pointer will cause a panic. So although we can use nil to represent a null value, it clearly has some significant flaws. We need to check if the pointer is null before using it.

Additionally, if this struct is intended to be scanned by a sql query, we can use sql.NullString or the package null.

sql.NullString / sql.NullInt, etc.

If we intend to map the values of our defined struct to a database, we need to implement an interface called Scanner. If a string is null, it cannot be mapped to a value in the SQL database. sql.NullString implements the Scanner interface. If directly converted to JSON, it will look like this:

{
  "email": {
    "Valid": true,
    "String": "xxx@gmail.com"
  }
}

You would need to use MarshalJSON to convert it to the correct JSON format (although it can be left as is).

null

The null package helps define common data types, making it convenient 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. Rewrite MarshalJSON

In golang, we can define a custom type for primitive types and add methods to it. We can rewrite MarshalJSON to make the default output, which should be the zero value, null. For example:

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 value + tag

Golang does not have syntactic sugar like higher-level languages, such as:

const getPerson = (id = 1, name = '') => (
)
def get_person(id=1, name='', is_admin=False):
    ...

It also doesn't have a convenient ternary operator to help with default value checks. Therefore, implementing default values in golang is a bit more complicated. Someone once asked on a forum about this, but the request was rejected for reasons like:

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 not 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.

The reasons are probably that Google does not want you to pass a bunch of func(nil, nil, nil, nil) just to make the default value work, or you can use dynamic args… to calculate the number and types of parameters without needing default values.

But what if we really need default values? I don't want the initialization to be 0, "" etc. What can we do? In such cases, we can try using golang's built-in tag feature, for example:

type People struct {
  Name string `default:""`
  Age int64 `default:10`
}

to achieve this, but implementing a complete tag feature requires careful consideration. I'll try implementing it another day.

Conclusion

This article attempts to summarize the issues that may arise when using default values and zero values in golang, and tries to propose solutions.

Prev

How to collect and centralize logs in golang app

Next

Implementing the pin to bottom component via overflow-anchor

If you found this article helpful, please consider buy me a drink ☕️ It'll make my ordinary day shine✨

Buy me a coffee

作者

Kalan 頭像照片,在淡水拍攝,淺藍背景

愷開 | Kalan

Hi, I'm Kai. I'm Taiwanese and moved to Japan in 2019 for work. Currently settled in Fukuoka. In addition to being familiar with frontend development, I also have experience in IoT, app development, backend, and electronics. Recently, I started playing electric guitar! Feel free to contact me via email for consultations or collaborations or music! I hope to connect with more people through this blog.