Functional Programming in Go: Principles and Examples

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Functional Programming in Go: From Imperative to Declarative



Go (Golang) is traditionally associated with imperative and concurrent programming, however its powerful type system and support for first-class functions make it possible to apply many principles of functional programming (FP). In this article, we will examine how to apply FP idioms in Go without sacrificing performance and code readability.



Basic Principles of FP in the Context of Go



Functional programming in Go does not mean a complete rejection of mutable state. Instead, we use the following concepts:



  • Pure functions — functions whose result depends only on arguments, without side effects.
  • Immutability — preferring to create new data structures instead of modifying existing ones.
  • Higher-order functions — functions that take other functions as arguments or return them.
  • Closures — functions that capture variables from the outer scope.


Higher-order functions: map, filter, reduce



Go does not have built-in generics (until version 1.18), so classic map, filter, reduce are implemented via interfaces or explicit copying. Let's look at an example with integers:



package main

import "fmt"

// Map — applies a function to each element of a slicefunc Map(s []int, fn func(int) int) []int { result := make([]int, len(s)) for i, v := range s { result[i] = fn(v) } return result}

// Filter — keeps only elements that satisfy the conditionfunc Filter(s []int, fn func(int) bool) []int { var result []int for _, v := range s { if fn(v) { result = append(result, v) } } return result}

// Reduce — collapses a slice into a single valuefunc Reduce(s []int, init int, fn func(int, int) int) int { acc := init for _, v := range s { acc = fn(acc, v) } return acc}

func main() { nums := []int{1, 2, 3, 4, 5}

// Double each element doubled := Map(nums, func(x int) int { return x * 2 }) fmt.Println("Map:", doubled) // [2 4 6 8 10]

// Keep only even numbers evens := Filter(nums, func(x int) bool { return x%2 == 0 }) fmt.Println("Filter:", evens) // [2 4]

// Sum all elements sum := Reduce(nums, 0, func(acc, x int) int { return acc + x }) fmt.Println("Reduce:", sum) // 15}


Important: In Go 1.18+ with generics, you can make universal versions:



// Generic Mapfunc MapGeneric[T, U any](s []T, fn func(T) U) []U {    result := make([]U, len(s))    for i, v := range s {        result[i] = fn(v)    }    return result}


Closures and Currying in Go



Closures in Go are a powerful tool for creating functions with state preservation. Let's look at an example of a sequence generator:



package main

import "fmt"

// Fibonacci sequence generatorfunc fibonacci() func() int { a, b := 0, 1 return func() int { a, b = b, a+b return a }}

// Currying: a function that takes arguments in stagesfunc add(a int) func(int) int { return func(b int) int { return a + b }}

func main() { fib := fibonacci() fmt.Println(fib()) // 1 fmt.Println(fib()) // 1 fmt.Println(fib()) // 2

// Currying add5 := add(5) fmt.Println(add5(10)) // 15 fmt.Println(add5(20)) // 25}


Closures are especially useful for:

  • Creating configurable functions (middleware)
  • Implementing the "function factory" pattern
  • Organizing callback mechanisms


Immut

Blogs

Book Recommendations