Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Handling Optional Values in Go Using the samber/mo Library

Tech Oct 7 18

The samber/mo library brings functional programming abstractions, specifically monads like Option, to Go. A monad can be visualized as a wrapper that holds a value and defines a standard way to chain operations. This chain stops immediate if any step fails to produce a value, lifting the burden of repetitive nil checks and error propagation from the developer's logic. Option[T] represents a value of type T that may or may not exist, eliminating the direct use of nil pointers and making the data flow and potential absence of a value explicit in the type system.

Building Option Instances

Two primary constructors create an Option:

  • Some[T](value T): Wraps a concrete value into an Option that is present.
  • None[T](): Produces an Option that holds nothing.

A common pipeline might involve a sequence of transformations where each stage relies on the success of the previous one. The code below implements this pattern using a different domain: processing a user entity. The process attempts to fetch a user, then validates their profile, and finally generates a greeting. A failure at any point short-circuits the rest of the chain.

package main

import (
	"fmt"
	"strings"

	"github.com/samber/mo"
)

type Profile struct {
	Name  string
	Email string
}

func fetchUser(id int) mo.Option[Profile] {
	// Simulating a user found in the database
	return mo.Some(Profile{Name: "Alex", Email: "alex@example.com"})
}

func validateProfile(p Profile) mo.Option[Profile] {
	if strings.Contains(p.Email, "@") {
		return mo.Some(p)
	}
	// Validation fails, returning empty Option
	return mo.None[Profile]()
}

func formatGreeting(p Profile) mo.Option[string] {
	// Simulating a formatting operation that always succeeds
	greeting := fmt.Sprintf("Welcome, %s", p.Name)
	return mo.Some(greeting)
}

func main() {
	resultOption := mo.Some(42).FlatMap(func(id int) mo.Option[Profile] {
		return fetchUser(id)
	}).FlatMap(func(profile Profile) mo.Option[Profile] {
		return validateProfile(profile)
	}).FlatMap(func(profile Profile) mo.Option[string] {
		return formatGreeting(profile)
	})

	finalMessage, isValid := resultOption.Match(
		func(msg string) (string, bool) {
			return msg, true
		},
		func() (string, bool) {
			return "Could not generate greeting", false
		},
	)

	if isValid {
		fmt.Println(finalMessage)
	} else {
		fmt.Println(finalMessage)
	}
}

If the email validation fails in validateProfile, the None propagates, and formatGreeting is never invoked.

Core Operations on Option[T]

Beyond FlatMap, the library offers a rich set of methods to inspect, extract, and transform the contained value:

Inspecting State

  • IsPresent(): returns true if the Option holds a value.
  • IsAbsent(): returns true if the Option is empty.
  • Size(): yields 1 for a presant Option, 0 otherwise.

Extracting Values

  • Get(): returns the inner value and a boolean flag indicating presence.
  • MustGet(): returns the value but panics if the Option is None. Use only when certain of a value.
  • OrElse(fallback T): providess the wrapped value or a default if None.
  • OrEmpty(): returns the Option if present, or an empty Option for the same type.
  • ToPointer(): converts the inner value to a pointer; returns nil for None.

Applying Side Effects

  • ForEach(fn func(T)): executes the given function only if a value exists, otherwise does nothing.

Transforming

  • Map(fn func(T) T1): applies a transformation to the value if present, returning Some[T1]; otherwise returns None[T1].
  • MapNone(fn func() T): transforms an empty Option into Some[T]; does nothing if the Option is already present.
  • FlatMap(fn func(T) Option[T1]): similar to Map, but the transformation itself returns an Option, preventing nested Option[Option[T]].

Conditional Execution

  • Match(onPresent func(T) (R, bool), onNone func() (R, bool)): evaluates one of two functions and returns both a result and a flag indicating which path was taken.

Serialization

  • MarshalJSON(): serializes the value as JSON; for None, it produces null.
  • UnmarshalJSON(data []byte): deserializes JSON into an Option.

Database Integration

  • Scan(src interface{}): implements sql.Scanner, allowing a database NULL to become a None.
  • Value(): implements driver.Valuer, converting a None into an SQL NULL.

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.