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