Functional Programming in Go: Advanced Techniques and Examples
Go (Golang) was not originally designed as a purely functional language, but its powerful features such as closures, first-class functions, and goroutines allow you to successfully apply the functional programming (FP) paradigm at an advanced level. This article is intended for developers already familiar with the basics of Go who want to delve deeper into functional techniques for writing more predictable, testable, and concurrent code. We will look at currying, function composition, working with monad-like structures, and integrating FP with goroutines.
1. Currying and Partial Application of Functions
Currying is the transformation of a function that takes multiple arguments into a sequence of functions, each taking a single argument. In Go, which has no built-in support for currying, we implement it through closures.
Example: currying for a configurable logger
package main
import "fmt"
// Curried function: prefix -> message -> outputfunc createLogger(prefix string) func(string) { return func(message string) { fmt.Printf("[%s] %s", prefix, message) }}
func main() { infoLogger := createLogger("INFO") errorLogger := createLogger("ERROR")
infoLogger("System started") errorLogger("Connection error") // Output: // [INFO] System started // [ERROR] Connection error}Partial application is a special case of currying where we fix some of the arguments. In Go, this is especially useful for creating specialized versions of functions.
// Partial application: fix the coefficientfunc multiply(a, b int) int { return a * b}
func partialMultiply(a int) func(int) int { return func(b int) int { return multiply(a, b) }}
func main() { double := partialMultiply(2) triple := partialMultiply(3)
fmt.Println(double(5)) // 10 fmt.Println(triple(5)) // 15}2. Function Composition and Pipelines
Composition is combining simple functions into more complex ones. In Go, we can build chains of transformations by passing the result of one function as input to another. This works especially powerfully in combination with interfaces and generics (Go 1.18+).
Example: composition for string processing
package main
import ( "fmt" "strings" "unicode")
type StringTransform func(string) string
// Compose creates a composition of two functionsfunc Compose(f, g StringTransform) StringTransform { return func(s string) string { return f(g(s)) }}
// Pipeline combines multiple functionsfunc Pipeline(transforms ...StringTransform) StringTransform { return func(s string) string { for _, t := range transforms { s = t(s) } return s }}
func removeSpaces(s string) string { return strings.ReplaceAll(s, " ", "")}
func toUpper(s string) string { return strings.ToUpper(s)}
func removeDigits(s string) string { var result strings.Builder for _, r := range s { if !unicode.IsDigit(r) { result.WriteRune(r) } } return result.String()}
func main() { // Composition of two functions cleanAndUpper := Compose(removeSpaces, toUpper) result1 := cleanAndUpper("go 1.18 rocks") fmt.Println(result1) // "GO1.18ROCKS"
// Pipeline of three functions pipeline := Pipeline(removeSpaces, removeDigits, toUpper) result2 := pipeline("go 1.18 rocks") fmt.Println(result2) // "GOROCKS"}This approach allows you to build complex transformations from simple, testable blocks. Each pipeline function can be tested separately.