Let’s order a pizza in Go — Null fields

Adding null fields to our gorm models

Luis Masuelli
4 min readMay 13, 2019

Our models may have optional fields. You can have two kinds of optional values in your (logical) model design:

  • Allowing a null value in your column. This is the most sensible one and perhaps you did this using other libraries, ORMs, complete frameworks or even raw SQL in college.
  • Having, by convention, a particular value representing some empty or null value (yes! people doing stuff like this exists, and Golang’s type system is prone to do this for simplicity — please, don’t fall in the trap; it’s a tempting one).

This choice is not as easy as it sounds. Let’s start.

All the primitive scalar types in Go are appropriately mapped to the different field types of different database dialects:

  • Strings are mapped to varchar(255) unless told otherwise. You can change it using a gorm configuration like size:100 (to make them a varchar of length 100), or its entire type like type:text.
  • Integers are mapped as the corresponding integer types in databases.
  • Float (both 32 and 64 bits) and decimal values are also mapped accordingly.
  • Boolean values are also mapped, although each database implements it differently.

But those types do not allow nil as value in the Go side. This is a very basic aspect of Go, in contrast to other languages like C# or Java:

  • Strings and (boxed) primitive types can be assigned a null value in Java.
Integer foo = 5;
Integer bar = null;
String baz = null;
  • Strings can have null values in C#.
String foo = null;
String bar = "hello";
  • Neither is possible in Go.
// this won't compile
var foo int = nil
// neither this
var bar string = nil

So we need a workaround to work with null values in scalar fields, which is broken in two parts:

  • Our column must not have a not null specifier if we want the null value at database level (which will be, most likely, our scenario).
  • Our column must use an type (we’ll dig into this now).

Choosing an appropriate nullable type

If we want to add nullable scalar fields we have two options:

  • Pointer types to primitive values.
  • Standard sql null-types.

Let’s say you have a standard model like this:

type Foo struct {
ID uint
}

We’ll work by building examples based on this model.

Pointer types

They look quite idiomatic. While you’d declare a regular scalar field like this:

type Foo struct {
ID uint
Notes string `gorm:"size:100;not null"`
}

The nullable version would use a pointer instead:

type Foo struct {
ID uint
Notes *string `gorm:"size:100"`
}

While pointers are more in the style of what you’ll do with nullable associations, it’s a bit annoying to create a pointer to primitive types, and Go doesn’t have boxing types or functions to wrap primitive values in pointers. Perhaps you’d like to create some boxing function(s) like:

func BoxString(x string) *string {
// Remember! This is Go. You will not
// have dangling pointers by taking
// the pointer from a parameter.
return &x
}

to be used like this:

foo := Foo{1, BoxString("Content for the nullable string")}
// Or
foo := &Foo{1, BoxString("Content for the nullable string")}

Just remember to appropriately dereference the value when using it:

if foo.Notes != nil {
fmt.Println("Notes: " + *foo.Notes)
} else {
fmt.Println("No notes are set")
}

Nullable types

I still say… no. These nullable types have nothing to do with C# Nullable types or the Java’s boxed types. They distinguish from the formers in that Go doesn’t have generics, and they distinguish from the latters in that Go doesn’t have an equivalent of Java’s Integer and, even worse, a mean for autoboxing.

There’s a standard package named datbase/sql. Among different database-related features, it provides few nullable types whose usages are also supported in gorm:

  • sql.NullBool is a nullable bool.
  • sql.NullString is a nullable string.
  • sql.NullFloat64 is a nullable float64. There is no nullable type for float32.
  • sql.NullInt64 is a nullable int64. There is no nullable type for other integer types.

If we have our initial model:

type Foo struct {
ID uint
Notes string `gorm:"size:100;not null"`
}

the nullable version would use the corresponding structure instead:

type Foo struct {
ID uint
Notes sql.NullString `gorm:"size:100"`
}

And you use them like this:

foo := Foo{1, sql.NullString{String: "Content for the nullable string", Valid: true}}
// Or
foo := &Foo{1, sql.NullString{String: "Content for the nullable string", Valid: true}}

When assigning a null value to the field, either will do the trick:

foo.Notes = sql.NullString{Valid: false}
// Or
foo.Notes.Valid = false

While this is an example using the sql.NullString type, the other 3 types work like that, except for the data member of each structure (being named Int64, Bool, and Float64).

Remember: you only need boxing functions for scalar types. struct types can be pointer-initialized with literals. This applies in particular to time.Time, which is scalar as a database field but it is not a scalar in Go.

Comparison and caveats

While the pointer-based option is clear, idiomatic, and similar to what can be done with associations, Go doesn’t have a comfortable way to instantiate pointers to those types on the fly as it has, in contrast, in structures. This works:

foo := &Foo{1, "Hello"}

while these ones will not work:

n := &1
n := &uint(1)
s := &"hello"
s := &string("hello")
// You get the idea

So you have to get or code your own boxing functions.

On the other hand, null types are quite annoyingly non-idiomatic, they lack of type diversity and they will trouble your JSON serialization.

Finally…

…it will be up to you whether to use the regular pointers or the null structures. They both work on gorm.

--

--