Can Go’s New Generics Unlock Functional Programming?
This article explores how Go 1.18’s generic type parameters enable functional programming features such as higher‑order functions, lazy evaluation, and partial application, while also highlighting the limitations and design compromises that arise from Go’s type system and language ergonomics.
Background
Functional programming (FP) offers statelessness, no side effects, concurrency friendliness, and high abstraction, and many popular languages (C++, Python, Rust) have adopted FP features. Go, however, has seen little discussion because of the lack of generic support, which made writing type‑agnostic functions difficult.
Recent proposals (e.g., spec: add generic programming using type parameters #43651 ) have been accepted, and Go 1.18 (early 2022) introduced generics, prompting a re‑examination of FP in Go.
Overview
This article incrementally implements common FP features using Go generics to evaluate their advantages and shortcomings. All code snippets are runnable (except for omitted package main and main declarations).
Generic Syntax
Functions can have a type‑parameter list in square brackets, e.g., func F[T any](p T) { ... }.
Type parameters may be used in function parameters and bodies.
Types can also have type parameters, e.g., type M[T any] []T.
Each type parameter has a constraint; any matches any type.
Constraints are expressed with interface, e.g., type Integer1 interface { int }.
Union types use |, e.g.,
type Integer2 interface { int | int8 | int16 | int32 | int64 }.
The ~T syntax matches underlying types, e.g., type Integer3 interface { ~int }.
This design has been proposed and accepted as a future language change. We currently expect that this change will be available in the Go 1.18 release in early 2022. Type Parameters Proposal
Higher‑Order Functions
Higher‑order functions (HOFs) either accept functions as arguments or return functions. Go supports closures, so HOFs are straightforward:
func foo(bar func() string) func() string {
return func() string {
return "foo" + " " + bar()
}
}
func main() {
bar := func() string { return "bar" }
foobar := foo(bar)
fmt.Println(foobar()) // Output: foo bar
}A generic Filter HOF can be written as:
func Filter[T any](f func(T) bool, src []T) []T {
var dst []T
for _, v := range src {
if f(v) {
dst = append(dst, v)
}
}
return dst
}
func main() {
src := []int{-2, -1, 0, 1, 2}
dst := Filter(func(v int) bool { return v >= 0 }, src)
fmt.Println(dst) // Output: [0 1 2]
}Code‑Generation Dilemma
Before generics, two approaches existed: using interface{} with reflection (sacrificing type safety and performance) or writing separate functions for each concrete type (high duplication). Code generation mitigates duplication but cannot handle arbitrary type combinations, especially for functions like Map that require multiple type parameters.
Generics allow a single signature such as func Map[T1, T2 any](f func(T1) T2, src []T1) []T2, avoiding the explosion of specialized functions.
Unsugared Generics
Go’s syntax is verbose compared to languages like Haskell. For example, anonymous functions in Haskell are concise, while Go requires full type signatures for closures, leading to boilerplate.
filter \x -> x >= 0 $ [-2,-1,0,1,2]
-- Output: [0,1,2]Proposals such as Lightweight anonymous function syntax aim to simplify closure syntax, but they are not expected before Go 2.
Method Type Parameters
Methods cannot have their own type parameters; only the receiver type may have them. This restriction simplifies the language but limits expressiveness for generic methods like Map on a generic list.
This design does not permit methods to declare type parameters that are specific to the method. The receiver may have type parameters, but the method may not add any type parameters.
Lazy Evaluation
Lazy evaluation defers computation until the result is needed, reducing space complexity. A lazy version of Add returns a closure:
func Add(a, b int) int { return a + b }
func LazyAdd(a, b int) func() int { return func() int { return a + b } }Chaining lazy Filter calls can keep only one slice in memory, achieving O(N) space instead of O(k·N).
type Iter[T any] interface { Next() (T, bool) }
type SliceIter[T any] struct { i int; s []T }
func IterOfSlice[T any](s []T) Iter[T] { return &SliceIter[T]{s: s} }
func (i *SliceIter[T]) Next() (v T, ok bool) {
if ok = i.i < len(i.s); ok {
v = i.s[i.i]
i.i++
}
return
}A lazy filterIter implements Iter by applying a predicate on demand:
type filterIter[T any] struct { f func(T) bool; src Iter[T] }
func (i *filterIter[T]) Next() (v T, ok bool) {
for {
v, ok = i.src.Next()
if !ok || i.f(v) {
return
}
}
}
func Filter[T any](f func(T) bool, src Iter[T]) Iter[T] { return &filterIter[T]{f: f, src: src} }Partial Application
Partial application fixes some arguments of a multi‑parameter function, returning a function that accepts the remaining arguments. Types such as FuncWith1Args and FuncWith2Args illustrate this concept.
type FuncWith1Args[A, R any] func(A) R
type FuncWith2Args[A1, A2, R any] func(A1, A2) R
func (f FuncWith2Args[A1, A2, R]) Partial(a1 A1) FuncWith1Args[A2, R] {
return func(a2 A2) R { return f(a1, a2) }
}Using generics, the boilerplate can be reduced by letting the compiler infer type arguments:
func Cast[A1, A2, R any](f FuncWith2Args[A1, A2, R]) FuncWith2Args[A1, A2, R] { return f }
f2 := Cast(Filter[int])Variadic Type Parameters
Go does not support variadic type parameters, so separate generic types must be written for each arity (e.g., FuncWith3Args, FuncWith4Args), increasing code duplication.
Type System
Many FP features rely on a powerful type system, which Go currently lacks. For example, compile‑time type inspection is impossible; developers must resort to runtime type assertions or reflect. The ~T syntax introduces the notion of “underlying type,” but it cannot be used in type assertions, limiting expressiveness.
type Signed interface { ~int }
func IsSigned[T Signed](n T) {
// cannot write: if _, ok := (interface{})(n).(Signed); ok { ... }
}These gaps prevent full implementation of advanced FP concepts such as type classes or algebraic data types.
Conclusion
Generics enable many FP features to be expressed more generally.
They offer greater flexibility than code generation, though numerous edge‑case issues remain.
Go 1.18’s generics add only the type‑parameter syntax without broader language changes, leading to some mismatches with existing language design.
Limitations of Go’s type system prevent a complete FP feature set.
References
Golang Functional Programming Brief – https://hedzr.com/golang/fp/golang-functional-programming-in-brief/
GopherCon 2020 – Functional Programming with Go – https://www.youtube.com/watch?v=wqs8n5Uk5OM
Spec: add generic programming using type parameters #43651 – https://github.com/golang/go/issues/43651
Type Parameters Proposal – https://go.googlesource.com/proposal/+/refs/heads/master/design/43651-type-parameters.md
Compile Go from source – https://golang.org/doc/install/source#install
go2go Playground – https://go2goplay.golang.org/
#Very high level overview – https://go.googlesource.com/proposal/+/refs/heads/master/design/43651-type-parameters.md#very-high-level-overview
Higher‑order functions – https://zh.wikipedia.org/wiki/高阶函数
Code‑generation examples – https://github.com/golang/go/search?q=filename%3Agen.go
hasgo – https://pkg.go.dev/github.com/DylanMeeus/hasgo/types?utm_source=godoc#Ints.Map
functional‑go – https://pkg.go.dev/github.com/logic-building/functional-go/fp
fpGo – https://pkg.go.dev/github.com/TeaEntityLab/fpGo#Map
Lightweight anonymous function syntax proposal – https://github.com/golang/go/issues/21498
Method chaining – https://wikipedia.org/wiki/Method_chaining
#No parameterized methods – https://go.googlesource.com/proposal/+/refs/heads/master/design/43651-type-parameters.md#No-parameterized-methods
Partial application – https://wikipedia.org/wiki/Partial_application
Currying – https://zh.wikipedia.org/wiki/柯里化
Partial Function Application is not Currying – https://www.uncarved.com/articles/not-currying/
Closure syntax discussion – https://silverrainz.me/blog/funtional-programming-in-go-generics.html#id18
#Type inference – https://go.googlesource.com/proposal/+/refs/heads/master/design/43651-type-parameters.md#type-inference
#Type inference for composite literals – https://go.googlesource.com/proposal/+/refs/heads/master/design/43651-type-parameters.md#type-inference-for-composite-literals
#Omissions – https://go.googlesource.com/proposal/+/refs/heads/master/design/43651-type-parameters.md#omissions
Code‑generation dilemma – https://silverrainz.me/blog/funtional-programming-in-go-generics.html#id12
#Identifying the matched predeclared type – https://go.googlesource.com/proposal/+/refs/heads/master/design/43651-type-parameters.md#identifying-the-matched-predeclared-type
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
MaGe Linux Operations
Founded in 2009, MaGe Education is a top Chinese high‑end IT training brand. Its graduates earn 12K+ RMB salaries, and the school has trained tens of thousands of students. It offers high‑pay courses in Linux cloud operations, Python full‑stack, automation, data analysis, AI, and Go high‑concurrency architecture. Thanks to quality courses and a solid reputation, it has talent partnerships with numerous internet firms.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
