The story of a nil pointer receiver
Originally published on my personal blog
Let’s assume we want to store Gender
as an attribute for our users.
And since we use PostgreSQL we can define an enum for gender like this:
CREATE TYPE gender_type AS ENUM('MALE', 'FEMALE', 'OTHER');
Now let’s see how we should use gender_type
in our code. I always prefer to have as much similarity as possible between my models and my database design so I create a new type for Gender
as I did in the database and implemented the Scanner
interface:
package gender
import (
"database/sql/driver"
"errors"
"fmt"
)
// Gender type is nothing more than a string
type Gender string
const (
MALE Gender = "MALE"
FEMALE Gender = "FEMALE"
OTHER Gender = "OTHER"
)
func (g *Gender) Scan(src interface{}) (err error) {
switch val := src.(type) {
case []byte:
*g = Gender(string(val))
// OTHER CASES ...
default:
err = fmt.Errorf("unexpected type: %q", src)
}
return
}
func (g Gender) Value() (value driver.Value, err error) {
if g == MALE || g == FEMALE || g == OTHER {
return string(g), nil
}
return nil, fmt.Errorf("Invalid Gender value: %q", g)
}
Now let’s use our Gender
type in the user model:
package model
type User struct {
ID int
Name string
// ...
Gender gender.Gender `sql:"type:gender_type"`
}
and everything works fine now…except when we want to be able to have NULL values for gender and we modify the model to this
Gender *gender.Gender `sql:"type:gender_type"`
Now when we create an empty instance of the user model to scan from the database, the Gender
field is nil
, and when the Scan
method is called the pointer receiver(g) is nil
too. When we try to dereference the g
to update its value we receive panic: runtime error: invalid memory address or nil pointer dereference
.
The instant solution that comes to mind is to do this:
// instead of *g = Gender(string(val))
g = &Gender(string(val))
But as soon as you find this solution, you find it useless because this is what happens.
- You create an empty instance of the user so the gender field is a nil pointer (a pointer pointing to nothing)
- You call the scan method and Go sees that you want a pointer receiver, so it creates a new pointer pointing the the value of gender(which is currently
nil
) - You create a new
Gender
instance and put its address in the pointer you received. - But this is the new pointer that Go created for you and the pointer in the user model is still untouched
- And BOOM!
Conclusion
Be really careful with these pointer receivers and as long as you can don’t dereference a pointer receiver!
I’d be glad to see what you think of the problem and what’d be your solution.
P.S.
The solution I used was to look for other similar types out there and see what they do and here is what they do:
https://go.dev/src/database/sql/sql.go#:~:text=type-,NullString,-struct%20%7B