Three common practices exist to indicate the lack of a value.
zero value
nil pointer
false boolean value
If the zero value is a valid value, than the non-zero the zero value approach is simplest, because it requires nothing, but isn’t viable if the zero value for your type is a valid value.
The nil pointer approach works with the potential downside of heap allocation.
The boolean approach is used where the zero value is a valid value but heap allocation isn’t necessarily desirable. Various libraries use this approach, including database/sql. This approach has the benefit of clarity at the cost of verbosity. That is, it’s obvious to readers that the value is valid or present or not without having to make any inferences based on if the pointer is nil or the zero value.
Adding to the excellent replies already here: you could also use the new-ish generic Null type from the database/sql package:
// Null represents a value that may be null.
// Null implements the [Scanner] interface so
// it can be used as a scan destination:
//
// var s Null[string]
// err := db.QueryRow("SELECT name FROM foo WHERE id=?", id).Scan(&s)
// ...
// if s.Valid {
// // use s.V
// } else {
// // NULL value
// }
type Null[T any] struct {
V T
Valid bool
}
So you could refactor your user struct to something like this:
type User struct {
Age sql.Null[int]
}
This supports scanning from SQL. The only negative is: if I use this approach I usually implement a custom JSON marshal if JSON is my eventual destination because on the client I just want a nullable int. For that reason I tend towards just using a pointer. OR - best case zero value where 0 is not a valid value (IDs and such).