Using Pointer in Go is sometimes inevitable. One case is when I started to learn how to use Go. At that time, the team was migrating the legacy Node.js Backend application to Go. The frontend application is using Javascript.
One nature that was different at that time was dynamic typing language vs static typing language. One thing that is quite annoying is the ability of dynamic-type language to use partial objects to update data.
//let's say all fields in the database table are like this
const user = {
id: 1,
name: 'John',
address: 'Indonesia',
phone: '+62 881 112 312 xxx',
isBanned: true
}
// then from frontend updated like this
const updateInput = {
name: 'Erick'
}
// update
const updatedUser = { ...user, ...updateInput }
console.log(updatedUser)
// Output:
// {
// address: "Indonesia",
// id: 1,
// isBanned: true,
// name: "Erick",
// phone: "+62 881 112 312 xxx"
// }
This works fine when the data communication happens between two dynamic-typed languages. But the introduction of Go to the Playground brings a headache. The decision at that time was we were not allowed to change the front end because if it still worked fine, don't touch it.
So, we need to take the burden to the backend. It's challenging to do because Go uses default values for its data types.
package main
import "fmt"
type User struct {
ID int
Name string
Phone string
Address string
IsBanned bool
}
func (u *User) Update(input User) {
u.Name = input.Name
u.Phone = input.Phone
u.Address = input.Address
u.IsBanned = input.IsBanned
}
func main() {
// Imagine this is current user data from the database
user := User{
ID: 1,
Name: "John",
Phone: "+62 881 112 312 xxx",
Address: "Indonesia",
IsBanned: true,
}
//This is the update payload
payload := User{
Name: "Eric",
}
user.Update(payload)
fmt.Printf("%+v\n", user)
}
// Output: {ID:1 Name:Eric Phone: Address: IsBanned:false}
This becomes a headache for people who come from dynamic typing language, and things don't work out. The result in the Go source code is that Go treats the default value as the same as the regular value so that it will replace the previous value.
To resolve this issue, we need something that can mimic the behavior of an object in dynamic typing language. This is when a Pointer comes in. A pointer can indicate the absence of a field in a struct and avoid using a default value (maybe not entirely).
package main
import "fmt"
type User struct {
ID *int
Name *string
Phone *string
Address *string
IsBanned *bool
}
func (u *User) Update(input User) {
if input.Name != nil {
u.Name = input.Name
}
if input.Phone != nil {
u.Phone = input.Phone
}
if input.Address != nil {
u.Address = input.Address
}
if input.IsBanned != nil {
u.IsBanned = input.IsBanned
}
}
func main() {
// Imagine this is current user data from the database
ID := 1
name := "John"
phone := "+62 881 112 312 xxx"
address := "Indonesia"
isBanned := true
user := User{
ID: &ID,
Name: &name,
Phone: &phone,
Address: &address,
IsBanned: &isBanned,
}
//This is the update payload
updatedName := "Eric"
payload := User{
Name: &updatedName,
}
user.Update(payload)
fmt.Printf("%+v\n", user)
}
// Output: {ID:0xc0000aa010 Name:0xc000096070 Phone:0xc000096050 Address:0xc000096060 IsBanned:0xc0000aa018}
That is the code that is updated using Pointer. But what the hell is going on with the output? That happened because the pointer holds a memory address, and the fmt.Printf
cannot handle that directly. So there is extra work that needs to be done.
package main
import (
"encoding/json"
"fmt"
)
type User struct {
ID *int
Name *string
Phone *string
Address *string
IsBanned *bool
}
func (u User) String() string {
bytes, _ := json.Marshal(u)
return string(bytes)
}
func (u *User) Update(input User) {
if input.Name != nil {
u.Name = input.Name
}
if input.Phone != nil {
u.Phone = input.Phone
}
if input.Address != nil {
u.Address = input.Address
}
if input.IsBanned != nil {
u.IsBanned = input.IsBanned
}
}
func main() {
// Imagine this is current user data from the database
ID := 1
name := "John"
phone := "+62 881 112 312 xxx"
address := "Indonesia"
isBanned := true
user := User{
ID: &ID,
Name: &name,
Phone: &phone,
Address: &address,
IsBanned: &isBanned,
}
//This is the update payload
updatedName := "Eric"
payload := User{
Name: &updatedName,
}
user.Update(payload)
fmt.Printf("%+v\n", user)
}
// Output: {"ID":1,"Name":"Eric","Phone":"+62 881 112 312 xxx","Address":"Indonesia","IsBanned":true}
Finally, everything that is required for the Backend migration is met.
But, there is something still bogging me. Using a pointer is considered harmful because improper use can lead to nil dereference error (in other programming languages, it might be known as Null Pointer Exception). This error can stop the entire program.
We need to carefully check the value before using it.
if input.Name != nil {
u.Name = input.Name
}
if input.Phone != nil {
u.Phone = input.Phone
}
if input.Address != nil {
u.Address = input.Address
}
if input.IsBanned != nil {
u.IsBanned = input.IsBanned
}
Assigning value is cumbersome because we need to declare a variable before we can use the value.
ID := 1
name := "John"
phone := "+62 881 112 312 xxx"
address := "Indonesia"
isBanned := true
user := User{
ID: &ID,
Name: &name,
Phone: &phone,
Address: &address,
IsBanned: &isBanned,
}
We need a way to resolve this issue.
Using a simple utility function to make it safer and easier
// get the memory address of a value
func Ptr[T any](value T) *T {
return &value
}
// get the value of the pointer, or else use another value
func ValOrElse[T any](ptr *T, defaultVal T) T {
if ptr == nil {
return defaultVal
}
return *ptr
}
// set pointer value safely
func SetPtr[T any](ptr **T, newPtr *T) {
if ptr == nil || newPtr == nil {
return
}
if *ptr == nil {
*ptr = new(T)
}
**ptr = *newPtr
}
By using the functions above, we can use pointers safer or at least easier than the previous.
package main
import (
"encoding/json"
"fmt"
)
type User struct {
ID *int
Name *string
Phone *string
Address *string
IsBanned *bool
IsActive *bool
}
func (u User) String() string {
bytes, _ := json.Marshal(u)
return string(bytes)
}
func (u *User) Update(input User) {
SetPtr(&u.Name, input.Name)
SetPtr(&u.Phone, input.Phone)
SetPtr(&u.Address, input.Address)
SetPtr(&u.IsBanned, input.IsBanned)
}
func main() {
// Imagine this is current user data from the database
user := User{
ID: Ptr(1),
Name: Ptr("Name"),
Phone: Ptr("+62 881 112 312 xxx"),
Address: Ptr("Indonesia"),
IsBanned: Ptr(true),
}
//This is the update payload
payload := User{
Name: Ptr("Eric"),
}
user.Update(payload)
fmt.Printf("%+v\n", user)
fmt.Println("is user active?", ValOrElse(user.IsActive, false))
}
// get the memory address of a value
func Ptr[T any](value T) *T {
return &value
}
// get the value of the pointer, or else use another value
func ValOrElse[T any](ptr *T, defaultVal T) T {
if ptr == nil {
return defaultVal
}
return *ptr
}
// set pointer value safely
func SetPtr[T any](ptr **T, newPtr *T) {
if ptr == nil || newPtr == nil {
return
}
if *ptr == nil {
*ptr = new(T)
}
**ptr = *newPtr
}
// Output:
// {"ID":1,"Name":"Eric","Phone":"+62 881 112 312 xxx","Address":"Indonesia","IsBanned":true,"IsActive":null}
// is user active? false
Using some of the utility above might be subjective, but you can use it if you think the same. For me, it helps me to worry less about nil pointer issues.
Using a pointer above might not be the best way to handle my case. I think a better way is to use an approach using a type like sql.NullString
. But that will be a topic for the next discussion.
That's all from me. Stay tuned.
Thank you.
Top comments (1)
Nice and simple, though I wondered when simple was going to occur while reading the long article. ๐
Of course
ValOrElse()
is a tortured name; I'd just go with theValue()
as with two parameters theElse
is really kinda implied.