Functional Programming in Go: From Theory to Practice
Go (Golang) was not originally designed as a purely functional language. However, its powerful type system, support for first-class functions, and closures allow you to successfully apply the principles of functional programming (FP). In this article, we will explore how to use a functional style in Go to write more predictable, testable, and concurrency-safe code.
1. First-Class Functions and Higher-Order Functions
In Go, functions are first-class objects. This means they can be assigned to variables, passed as arguments, and returned from other functions. Functions that work with other functions are called higher-order functions.
Example: passing a function as an argument
package main
import "fmt"
// Higher-order function: takes a function and applies it to a numberfunc applyOperation(x int, op func(int) int) int { return op(x)}
func double(x int) int { return x * 2}
func square(x int) int { return x * x}
func main() { result1 := applyOperation(5, double) result2 := applyOperation(5, square)
fmt.Println("Double:", result1) // 10 fmt.Println("Square:", result2) // 25}Example: returning a function from a function (function factory)
package main
import "fmt"
// Returns a function that adds a given valuefunc createAdder(x int) func(int) int { return func(y int) int { return x + y }}
func main() { addFive := createAdder(5) addTen := createAdder(10)
fmt.Println(addFive(3)) // 8 fmt.Println(addTen(3)) // 13}2. Closures in Go
A closure is a function that "remembers" variables from its outer scope, even after that scope has finished executing. Closures are a powerful tool for creating encapsulated state and function factories.
Example: sequence generator
package main
import "fmt"
func sequenceGenerator() func() int { i := 0 return func() int { i++ return i }}
func main() { next := sequenceGenerator()
fmt.Println(next()) // 1 fmt.Println(next()) // 2 fmt.Println(next()) // 3
// New generator starts from 1 next2 := sequenceGenerator() fmt.Println(next2()) // 1}Important: Be careful with closures in loops. In older versions of Go (before 1.22), the loop variable i was reused, which led to errors. Starting from Go 1.22, loop variables are captured by value.
3. Pure Functions and Immutability
A pure function is a function that:
- Always returns the same result for the same arguments.
- Has no side effects (does not modify external state, does not write to files/network).
Go does not have built-in support for immutability, but we can follow this principle ourselves.
Example: pure vs impure function
package main
import "fmt"
// IMPURE function: modifies an external slicevar counter int
func impureAddToSlice(s []int, val int) []int { counter++ // side effect return append(s, val)}
// PURE function: creates a new slicefunc pureAddToSlice(s []int, val int) []int { newSlice := make([]int, len(s)+1) copy(newSlice, s) newSlice[len(s)] = val return newSlice}
func main() { original := []int{1, 2, 3}
// Pure approach result := pureAddToSlice(original, 4) fmt.Println("Original:", original) // [1 2 3] - unchanged fmt.Println("Result:", result) // [1 2 3 4]}Tip: For working with immutable struct