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.