Understanding Go's Optional and Default Parameters

There is no direct support for optional/default function parameters in Go. But there are ways we can do it. Let's look over how.

Checking default value

We can create a function to pass the empty value and check inside the function, let's look at the example, valid only if the default value of the data type is invalid.

package main

import "fmt"

type User struct {
    Name  string
    Class int
    Age   int
}

const (
    DefaultClass = 1
    DefaultAge   = 25
)

func NewUser(name string, class, age int) *User {
    u := &User{
        Name:  name,
        Class: DefaultClass,
        Age:   DefaultAge,
    }
    if class != 0 {
        u.Class = class
    }
    if age != 0 {
        u.Age = age
    }

    return u
}

func main() {
    fmt.Printf("Default st: %#v\n", NewUser("samit", 0, 0))
    fmt.Printf("One options;: %#v\n", NewUser("samit", 6, 0))
    fmt.Printf("All params: %#v", NewUser("samit", 5, 25))
}

Output

Default st: &main.User{Name:"samit", Class:1, Age:25}
One options;: &main.User{Name:"samit", Class:6, Age:25}
All params: &main.User{Name:"samit", Class:5, Age:25}

Here we pass the empty int value which is 0 for the NewUser function but this can be challenging when we need to pass 0 as a value.

Using Pointers

Like Above example we create function to pass the params as pointer and check the nil value. If the value is nil or zero we use the default value else update the value with the given one.

func NewUser(name string, class, age *int) *User {
    u := &User{
        Name:  name,
        Class: DefaultClass,
        Age:   DefaultAge,
    }
    if class != nil && *class != 0 {
        u.Class = *class
    }
    if age != nil && *age != 0 {
        u.Age = *age
    }

    return u
}

func main() {
    fmt.Printf("Default st: %#v\n", NewUser("samit", nil, nil))
    fmt.Printf("One options: %#v\n", NewUser("samit", ptrInt(6), nil))
    fmt.Printf("All params: %#v", NewUser("samit", ptrInt(5), ptrInt(30)))
}

Outputs:

Default st: &main.User{Name:"samit", Class:1, Age:25}
One options;: &main.User{Name:"samit", Class:6, Age:25}
All params: &main.User{Name:"samit", Class:5, Age:30}
# Full code: https://go.dev/play/p/mPZ9oSfD0Ew

Using the Variadic function

Simple Function

Go supports the validation function. Variadic functions support any number of arguments.

The most common example is print() function. We can pass any number of arguments.

How to use it for optional parameters.

package main

import "fmt"

type User struct {
    Name  string
    Class int
}

const DefaultClass = 1

func main() {
    fmt.Printf("Default user: %#v\n", NewUser("samit"))
    fmt.Printf("User with class: %#v", NewUser("samit", 5))
}

func NewUser(name string, class ...int) *User {
    u := &User{
        Name:  name,
        Class: DefaultClass,
    }

    if len(class) > 0 {
        u.Class = class[0]
    }
    return u
}

In the above example, we are using a variadic function NewUser with the first param name and next class, class here is optional, and when the value is sent it will assign the first value sent in class.

Output

Default st: &main.User{Name:"samit", Class:1}
st: &main.User{Name:"samit", Class:5}

When NewUser("samit") is sent with name only it will add the default class as 1. Similarly, with class value NewUser("samit", 5), it returns with the sent class.

b. Functional Option Pattern

We can use a functional options pattern. It provides more flexibility on what params can be sent.

type User struct {
    Name  string
    Class int
    Age   int
}

const (
    DefaultClass = 1
    DefaultAge   = 25
)

type UserOption func(u *User)

func WithAge(age int) UserOption {
    return func(u *User) {
        u.Age = age
    }
}

func WithClass(class int) UserOption {
    return func(u *User) {
        u.Class = class
    }
}
func NewUser(name string, opts ...UserOption) *User {
    u := &User{
        Name:  name,
        Class: DefaultClass,
        Age:   DefaultAge,
    }
    for _, opt := range opts {
        opt(u)
    }
    return u
}

We have defined a new type called UserOption which can modify the user data as it takes reference to *User.

We have Two function which returns UserOption

WithAge(age int) which takes the age as params and WithClass(class int) which takes class as params.

We have NewUser function which initiates the user with the name and options with multiple user option as parameter.

So we can add any number of attributes to user with UserOption type and initiate accordingly.
Let's test this with

func main() {
    fmt.Printf("Default st: %#v\n", NewUser("samit"))
    fmt.Printf("One options;: %#v\n", NewUser("samit", WithClass(5)))
    fmt.Printf("All params: %#v", NewUser("samit", WithClass(5), WithAge(27)))
}

This prints out

Default st: &main.User{Name:"samit", Class:1, Age:25}
One options;: &main.User{Name:"samit", Class:5, Age:25}
All params: &main.User{Name:"samit", Class:5, Age:27}
# Live https://go.dev/play/p/oBdAOKYJ0eq

As go does not support optional parameter, the choice is according to the usecase.