paint-brush
Pointer Pitfalls: A Simple Utility Function That Makes Pointers in Go Easier by@labasubagia
355 reads
355 reads

Pointer Pitfalls: A Simple Utility Function That Makes Pointers in Go Easier

by Laba SubagiaFebruary 15th, 2024
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

Three functions: get pointer address, get value with fallback when nil, and set pointer value safely
featured image - Pointer Pitfalls: A Simple Utility Function That Makes Pointers in Go Easier
Laba Subagia HackerNoon profile picture


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.

If it works, 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 because 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 a nil dereference error (in other programming languages, it might be known as a 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.


Also published here.