Functional Programming in Go

Functional Kata Gopher
Matthias Koch | Delivery Hero
functional style since 2013 - Skala Java Kotlin Haskell-(EM) Go Dart

Ready To Relieve Your High School Algebra Nightmare?


\[ T(x) = \sum_{n=0}^\infty \frac{f^{(n)}(a)}{n!}(x-a)^n \]
Haskell Example:
							taylor :: (Double -> Double) -> (Double -> Double) -> Double -> Double -> Double taylor f f' a x = f a + f' a * (x - a)
						

Don't Worry, Gophers


It was a joke!


If we were algebra professionals, we wouldn't write Go code! 😉


We like things simple, easy, and small.

What This Presentation Is About

This presentation is not about the mathematics in functional programming.

Our brain (our natural language model) has at least
a 6-year advantage in training natural language over math.

It's about how functional programming principles can help you write better code:

  • Easy to understand.
  • Easy to maintain.
  • Easy to test.
Brain Natural vs Math

What Defines Functional Programming?

Core Principles

  1. Pure Functions

    Functions with predictable outputs and no side effects.

  2. Immutability

    Data cannot be modified after creation.

  3. First-Class and Higher-Order Functions

    Functions treated as first-class citizens, can be passed and returned.

  4. Composability

    Functions are composable, allowing small, simple functions to combine into complex functionality.

  5. Closures

    A closure is a function that captures its scope, allowing access to variables after the scope exits.

  6. Recursion over Loops

    Solving problems through recursive function calls instead of iterative loops.

  7. Referential Transparency

    Expressions can be replaced with their values without changing program behavior.

  8. Pattern Matching

    Powerful way to deconstruct and match data structures.

  9. Generics and Parametric Polymorphism

    Writing flexible, reusable code that works with multiple types.

  10. Algebraic Data Types (ADTs)

    Composing complex types from simpler ones.

Which Principles are not directly in Go?

  1. Immutability

    Data cannot be modified after creation.

  2. Recursion over Loops

    Solving problems through recursive function calls instead of iterative loops.

  3. Referential Transparency

    Expressions can be replaced with their values without changing program behavior.

  4. Pattern Matching

    Powerful way to deconstruct and match data structures.

  5. Algebraic Data Types (ADTs)

    Composing complex types from simpler ones.

Immutability

Definition: Data doesn't change after it's created.
Benefit: Ensures thread safety and avoids side effects.
Haskell
									let x = 5
									let y = x + 3 -- x remains unchanged
								
Go
									x := 5
									x = x + 3 // x is modified
								

Referential Transparency

Definition: Same input, same output.
Benefit: Easier reasoning and testing.

Haskell
									square x = x * x
									square 5 -- Always evaluates to 25
								
Go
									func square(x int) int {
										fmt.Println("Squaring", x) // Side effect
										return x * x
									}
								

Pattern Matching

Definition: Simplifies control flow by destructuring and analyzing data.

Haskell
									data Shape = Circle Float | Rectangle Float Float

									area :: Shape -> Float
									area (Circle r) = pi * r * r
									area (Rectangle w h) = w * h
								
Go
									type Shape interface{}
									type Circle struct { Radius float64 }
									type Rectangle struct { Width, Height float64 }

									func area(s Shape) float64 {
										switch shape := s.(type) {
											case Circle:
												return math.Pi * shape.Radius * shape.Radius
											case Rectangle:
												return shape.Width * shape.Height
											default:
												return 0
										}
									}
								

Everthing could be added here, no pattern matching, better use inteface with defined Area() function

Algebraic Data Types (ADTs)

Definition: Structuring data concisely using sum and product types.

Haskelldata Result = Success String | Failure String
Go
									type Result struct {
										Success bool
										Message string
									}
								

What Functional Principles are Available?

  1. Pure Functions

    with manual effort to avoid side effects.

  2. Composability

    manual composition.

  3. First-Class and Higher-Order Functions

    fully covered.

  4. Closures

    fully covered.

  5. Generics and Parametric Polymorphism

    introduced in Go 1.18+, excluding methods.

From Principles to Practice: Building Blocks


Pure Functions

Functions that always return the same output for the same input and produce no side effects.


						  f(x) → y   // Always same output for same input

						  func square(x int) int {   // A pure function in Go. It is deterministic and satisfies referential transparency.
						      return x * x
						  }

						  // For any x:
						  square(4)  // Always returns 16
						  square(4)  // Always returns 16
						  square(4)  // Always returns 16

						  // But real world has side effects:
						  // - File operations might fail
						  // - Network calls can timeout
						  // - Values might be missing
						  // - Operations can fail
						              ↓
						  function wrapValue(x) → box(x)   // What if we could extend this?
					  

Patterns for Composition


Values with a context
						Simple Value:     42
						With Context:     Result[42]  // Success(42) or Error("failed")
						                  Optional[42] // Some(42) or None()
					
In Go:
						type Result[T any] struct {
						    value T
						    err   error
						}

						// These containers let us:
						func Transform[T, U any](r Result[T], f func(T) U) Result[U]
						func Chain[T, U any](r Result[T], f func(T) Result[U]) Result[U]
					

These containers are called Monads.

What is a Monad?


Think of it as a container that follows three simple rules:

  1. It can wrap any value (like putting a gift in a box).
  2. You can transform the value (while it stays in the box).
  3. You can chain operations (passing the boxed value along).

See? Monads aren't scary - they're just smart boxes!

Practical Example


Error Handling

This is how a typical function call looks like in Go:
						value, err := doSomething()
						if err != nil {
						    return err
						}
						// use value
					

Let's Check Out the Code


→ Project Code

  • Typical code structure.
  • 3-layered architecture - data layer, business layer, API layer.
  • It simulates an app that can register users, and users can place an order with associated payments.
  • It contains logging and authentication middleware and simulates an authorization service.
  • Full unit test coverage.
  • GoLand/IntelliJ API calls available.


Continue Presentation

Let's Look at the Order Data Layer

orders_mem_storage.go
  • Simple in-memory storage for orders.
  • Classic Go layout with returning value and errors.

Let's Look at the Order Data Layer Unit Test

orders_mem_storage_test.go
  • Unit tests avoid any conditional logic inside the runner.
  • To make clear what is expected from each test, a validate function needs to be added.
  • That moves all the assertions into the test description and out of the runner.
  • Not ideal, but better compared to having conditions within the runner as it forces every new test case to think about the expected result.

How Monads Could Solve It

Switch to the Step 1 branch for the monads.

Let's create our own Result monad.

result.go

What is this Monad about?

  • It is a container for anything.
  • It encapsulates either a result value or an error and knows if it has a valid result or not.
  • It can be easily created with the OK or the Err (Errf) function.
  • It provides methods to provide information about the context: IsOk(), IsError().
  • It provides methods to retrieve its value: MustGet() and Error().

Let's Use this Monad in our Order Storage

orders_mem_storage.go

What has changed?

  • We only return a single value now, our Result Monad instead of two before (order and error).
  • We use the helper functions to construct the Monad.
  • Ok() for the positive case.
  • Errf() for the negative case.

Not a big change yet?

Let's See How This Affected Our Unit Tests

orders_mem_storage_test.go

What has changed here?

  • No more helper functions.
  • No more validate functions in the test description.
  • All assertions are now in the runner itself.
  • No conditional logic inside the runner.
  • A single assert covers it all.

You think about results in your test description and no more about validations and assertions.

Isn't that simpler and easier?

How Does This Change Affect the Service Layer?

orders_service.go

What has changed here?

  • We only receive the Result Monad.
  • So clients of order storage can no longer ignore a possible error.
  • We use a method now (not an expression) to ask the Result Monad about its context if there was an error.

Asking the monad about its result context is more natural language.

Isn't that simpler and easier?

What Have We Achieved? (1/2)

						
							// multiple return values
							GetOrder(ctx context.Context, orderID int) (*dsmodels.Order, error)
							                             ↓
							// return single box/container --> Monad
							GetOrder(ctx context.Context, orderID int) monads.Result[dsmodels.Order]
						
					

Only return a single entity


						validateOrderMatches := func(t *testing.T, expected, actual *dsmodels.Order, err error) {
							assert.NoError(t, err, "unexpected error received")
							assert.Equal(t, expected, actual, "expected order mismatch")
						}
						validateOrderNotFound := func(t *testing.T, actual *dsmodels.Order, err error) {
							assert.Nil(t, actual, "expected no order to be returned")
							assert.EqualError(t, err, errOrderNotFound, "expected error mismatch")
						}

						validate: func(t *testing.T, order *dsmodels.Order, err error) {
							validateOrderMatches(t, &dsmodels.Order{ID: 1, UserID: 123, Payments: []int{1, 2}}, order, err)
						},
														↓
						//it makes our unit tests much simpler
						expectedResult: monads.Ok(dsmodels.Order{ID: 1, UserID: 123, Payments: []int{1, 2}}),
					

It makes our unit tests much simpler.

What Have We Achieved? (2/2)


						//dsOrder, _ := service.storage.GetOrder(ctx, id)
						dsOrder, err := service.storage.GetOrder(ctx, id)
												↓
						dsOrderResult := service.storage.GetOrder(ctx, id)
					

Clients can no longer ignore errors.


						if err != nil
								↓
						if dsOrderResult.IsError()
					

Using a speaking method is closer to natural language for understanding what the code is about.

Let's Go to Step 2 and Talk About Chaining



Continue Presentation

Some Improvements Before We Go Further

orders_mem_storage.go

What has changed here?

  • Using a monad library instead of our own result Monad: github.com/samber/mo.
  • That library features many Monads with all instructions needed to make them powerful.

Let's Check How We Currently Process the Order

How do we convert the result from the storage layer and transform and enrich it to the required result of the service?

orders_service.go processOrder

What do we have here?

  • We have 5 return statements within a single function.
  • We call the authorization service.
  • We process the result from the authorization service.
  • We add the payment info from a separate function.
  • We add the user info from a separate function.
  • For every step, there is an expression to check if we can continue or not.

Wouldn't it be better if we had just a single return statement and a single check?

Could Monads Help Solve Our Previous Question?

What did we say about a Monad: it's a value with context.

Let's use this knowledge about context.

Result Monad Documentation

What do we have here?

  • Some more methods that deal with the context compared to what we created in the first step.
  • Let's look at Flatmap: FlatMap Documentation
  • It is a mapper if we have a valid result.
  • It uses the knowledge of the context to safely execute a mapping function if there is a valid result.
  • The mapper function itself should also return a ResultMonad, and FlatMap returns a Result Monad.
  • Let's check the example: FlatMap Playground

So what do we need to make use of it?

We Need More Monads

Let's change the functions to return Monads.

orders_service.go addUser

What do we have here?

  • We moved the authorization check into a function that returns a Monad.
  • The addPayment method now returns a Monad.
  • The addUser method now returns a Monad.

How do we make use of these?

Now That We Have Monads, Let's Chain Them

We now have 4 functions that all return a Result Monad.

orders_service.go processOrder

What happened here?

  • Because the Monad knows its context (was there an error or a valid result).
  • FlatMap executes the passed function based on the context.
  • Calling FlatMap preserves the context.
  • We can chain the calls to FlatMap safely.
  • In case of an error, we preserve it and return a safe result.

Summary

What happened here?

  • We composed small functions into more complex ones.
  • We chained the function calls.
  • We only need to check for a result in a single place.
  • We only have a single return statement.
  • In case of an error, we preserve it and return a safe result.

What Did We Achieve in Step 2? (Part 1)

				                
				                    // Authorization check
				                    isAuthorized, err := service.authorizationService.IsAuthorized(ctx, userId, order)
				                    if err != nil {
				                        return nil, err
				                    }
				                    if !isAuthorized {
				                        return nil, errors.New("user is not authorized to access this order")
				                    }
				                    // Add payments to the order
				                    order, err = service.addPayments(ctx, order)
				                    if err != nil {
				                        return nil, err
				                    }
				                    // Add user to the order
				                    order, err = service.addUser(ctx, order)
				                    if err != nil {
				                        return nil, err
				                    }
				                    return order, nil
				                
				            
				                
				                    // Authorization check
				                    return service.verifyAuthorization(ctx, userId, order).
				                        // Add payments to the order
				                        FlatMap(func(value *models.Order) mo.Result[*models.Order] {
				                            return service.addPayments(ctx, value)
				                        }).
				                        // Add user to the order
				                        FlatMap(func(value *models.Order) mo.Result[*models.Order] {
				                            return service.addUser(ctx, value)
				                        })
				                
				            
  • Using the benefit of Monads that know their context.
  • We composed small functions into a more complex one.

  • The code is more concise.
  • There is only a single return statement.
  • The code is easier to understand.
  • Read it like a book.

Not readable like a book?

What Did We Achieve in Step 2? (Part 2)

Imagine it like this:

				                
				                    // Authorization check
				                    return service.verifyAuthorization(ctx, userId, order).
				                        // Add payments to the order
				                        FlatMap(func(value *models.Order) mo.Result[*models.Order] {
				                            return service.addPayments(ctx, value)
				                        }).
				                        // Add user to the order
				                        FlatMap(func(value *models.Order) mo.Result[*models.Order] {
				                            return service.addUser(ctx, value)
				                        })
				                
				            
				                
				                    // Authorization check
				                    return service.verifyAuthorization(userId, order).
				                        // Add payments to the order
				                        FlatMap(service.addPayments).
				                        // Add user to the order
				                        FlatMap(service.addUser)
				                
				            

Better now?

But that's another story about how to remove the Context. ;)

Summary of Part 1

Functional Chain
  • We adopted the Result Monad to handle errors and separate business logic from error management.
  • Using Higher Order Functions and Function Composition, we built complex operations from simple, Pure Functions.
  • Through FlatMap and declarative pipelines, we replaced imperative error-checking with a clean, functional approach.

This approach provided:

  • Achieved a clear separation of concerns by isolating business logic from error handling.
  • Enabled a declarative, composable code structure for better readability and maintainability.
  • Ensured predictable function behavior, simplifying testing and debugging.

Code that is easier to read, test and maintain.

Thank you for your attention.
May your functions be pure, your chains unbroken, and your code as timeless as the wisdom of the masters.

Functional Kata Gopher
Matthias Koch | Delivery Hero
functional style since 2013 - Skala Java Kotlin Haskell-(EM) Go Dart