Skip to main content

Golang Generic Option Type

·655 words·4 mins
Sebastian Scheibe
Author
Sebastian Scheibe
Table of Contents

Introduction
#

You might have heard of the Option type, if not, let me introduce that concept shortly.

An Option type is data structure which either has a value or no value contained in itself, it kind of acts as a box (Schrödinger’s cat 😉). Usually, the Option type is created with a constructor such as Some(value) or None(). From there, the type usually offers methods which can act in case a value exists.

For example, IfPresent(consumer func (val T)) would execute the consumer if the value is present, otherwise nothing happens. Usually these methods can also be chained.

Now, with the latest release of Go 1.18 generics were introduced, which allow us to build a generic Option type. Let’s get into that! 🎉

Interface definition
#

As I am familiar with Java, I will take the Java definition of the Option type as an example.

package optional

type Option[T any] interface {
	Get() (*T, error)
	IsPresent() bool
	IfPresent(consumer func(val T)) Option[T]
	OrElse(other T) T
	OrElseGet(otherFunc func() T) T
	Filter(predicate func(val T) bool) Option[T]
}

Defined above is the interface definition, now we need to implement the methods for a struct that can represent the Option type.

Struct definition
#

Let’s get started with the struct itself.

type option[T any] struct {
	val *T // pointer to the type, so that NIL can represented
}

The option struct has the generic type definition [T any] which means, a type need to be defined at compile time before it can be used.

With the struct in place, we can go ahead and implement the functions which can create our struct.

func Of[T any](val T) Option[T] {
	return &option[T]{&val}
}

func Empty[T any]() Option[T] {
	return &option[T]{}
}

Perfect, we can create a struct but we are not yet done, we miss the methods definitions to satisfy the compiler.

Method definition
#

func (option *option[T]) Get() (*T, error) {
	if option.IsPresent() {
		return option.val, nil
	}
	return nil, errors.New("no value present")
}

func (option *option[T]) IsPresent() bool {
	return option.val != nil
}

func (option *option[T]) IfPresent(consumer func(val T)) Option[T] {
	val, _ := option.Get()
	if val != nil {
		consumer(*val)
	}
	return option
}

func (option *option[T]) OrElse(other T) T {
	val, _ := option.Get()
	if val != nil {
		return *val
	}
	return other
}

func (option *option[T]) OrElseGet(otherFunc func() T) T {
	val, _ := option.Get()
	if val != nil {
		return *val
	}
	return otherFunc()
}

func (option *option[T]) Filter(predicate func(val T) bool) Option[T] {
	val, _ := option.Get()
	if val != nil && predicate(*val) {
		return Of[T](*val)
	}
	return Empty[T]()
}

Mapper function
#

At last, we will add a mapper function, which will allow us to map one from one type to another type!

func Map[T any, S any](option Option[T], mapper func(val T) S) Option[S] {
	val, _ := option.Get()
	if val != nil {
		result := mapper(*val)
		return Of[S](result)
	}
	return Empty[S]()
}

With all functions and struct methods, we are able to work on the Option type!

Example Run
#

In this example, the main function calls the fetchURLContent function and returns an Option back. On that Option, the mapper is applied which takes return the length of the given string.

package main

import (
	"fmt"
	"go-optional-type/optional"
	"io/ioutil"
	"log"
	"net/http"
)

func fetchURLContent(url string) optional.Optional[string] {
	resp, err := http.Get(url)
	if err != nil {
		return optional.Empty[string]()
	}
	defer resp.Body.Close()
	html, err := ioutil.ReadAll(resp.Body)
	return optional.Of[string](string(html))
}

func main() {
	_, err := optional.Map[string, int](fetchURLContent("https://baidu.com"), func(val string) int {
		return len(val)
	}).IfPresent(func(val int) { fmt.Printf("len of string, %v", val) }).Get()
	
	if err != nil {
		log.Println("error fetching content")
	}
}

If all works out, it will return the length of the given URL!

➜  go-optional-type git:(main) ✗ go run main.go
len of string, 354217   

I hope you can get an impression on how generics can be applied in Go.

If you are interested in the source code, check here