Currying & Partial Application in Go
Turning Big Functions Into Tiny, Reusable Superpowers
“Small functions aren’t just cleaner — they’re easier to think with.”
I’ve always loved when code feels like Lego: tiny pieces snapping together into something bigger. Go doesn’t have built-in currying like Haskell or functional composition like Scala, but it does give you closures — and that’s enough to unlock a powerful pattern: currying and partial application.
These ideas sound academic, but in practice, they make your functions more reusable, your APIs cleaner, and your code easier to test. And once you get comfortable with them, you start structuring your programs in simpler, more composable ways without even realizing it.
Let’s dive in.
Currying vs Partial Application
Let’s keep definitions simple:
Currying: break a multi-argument function into a chain of single-argument functions.
Partial application: pre-fill some arguments to produce a new function.
Currying changes a function’s shape.
Partial application changes a function’s defaults.
In Go, both are achieved with closures.
Currying in Go (Yes, You Can)
Let’s start with a normal function:
func add(a, b int) int {
return a + b
}Currying rewrites this as a function that returns a new function:
func curryAdd(a int) func(int) int {
return func(b int) int {
return a + b
}
}Usage:
add5 := curryAdd(5)
fmt.Println(add5(10)) // 15
fmt.Println(add5(-3)) // 2You just created a new function — one that “remembers” the 5.
It’s simple, but it gives you something extremely powerful: customized, reusable behaviour.
Partial Application. The More Practical Pattern
Currying isn’t common in Go, but partial application absolutely is.
Example: imagine a logging function:
func logWith(level string, msg string) {
fmt.Printf(”[%s] %s\n”, level, msg)
}Partial application lets you pre-fill the log level:
func makeLogger(level string) func(string) {
return func(msg string) {
logWith(level, msg)
}
}Usage:
info := makeLogger(”INFO”)
warn := makeLogger(”WARN”)
error := makeLogger(”ERROR”)
info(”Service started”)
warn(”Cache miss”)
error(”Failed to connect to DB”)Cleaner. Less repetitive. Flexible.
I love it ❤️.
Parameterizing Behavior
Let’s say you have an HTTP request helper:
func fetch(url, token string) ([]byte, error) {
// pretend to fetch using the token
return []byte(”ok”), nil
}Partial application lets you create a version of this function tied to a specific API token:
func authFetch(token string) func(string) ([]byte, error) {
return func(url string) ([]byte, error) {
return fetch(url, token)
}
}Usage:
client := authFetch(”abcd123”)
client(”/users”)
client(”/posts”)
client(”/comments”)You’ve effectively built a tiny, functional “client object” — with no structs, no interfaces, no wrappers. Just closures.
Currying for More Expressive Pipelines
You can chain curried functions to express flow more clearly.
Example: filtering numbers.
A plain approach:
func greaterThan(n int, x int) bool {
return x > n
}Curried version:
func greaterThanC(n int) func(int) bool {
return func(x int) bool {
return x > n
}
}Now you can build expressive pipelines:
isAdult := greaterThanC(17)
fmt.Println(isAdult(20)) // trueOr apply it dynamically:
func filter(nums []int, f func(int) bool) []int {
var out []int
for _, n := range nums {
if f(n) {
out = append(out, n)
}
}
return out
}
nums := []int{10, 15, 20, 30}
fmt.Println(filter(nums, greaterThanC(18))) // [20 30]This pattern shines when you build small, composable components.
Partial Application With Multiple Parameters
Here’s a more advanced example: building an SQL executor with defaults.
func execQuery(db *sql.DB, query string, limit int) ([]Row, error) {
// run query with limit
return nil, nil
}Create a partially applied version:
func withDB(db *sql.DB) func(string, int) ([]Row, error) {
return func(query string, limit int) ([]Row, error) {
return execQuery(db, query, limit)
}
}Add another layer:
func withDefaultLimit(limit int, f func(string, int) ([]Row, error)) func(string) ([]Row, error) {
return func(query string) ([]Row, error) {
return f(query, limit)
}
}Usage:
dbExec := withDB(db)
smallQuery := withDefaultLimit(10, dbExec)
smallQuery(”SELECT * FROM users”)You essentially created a tiny DSL without needing new types.
Why Currying & Partial Application Matter in Go
They give you:
Cleaner, smaller functions
Explicit customization via closures
Reusable and testable building blocks
A functional flavor without sacrificing Go’s simplicity
You’re not trying to turn Go into Haskell. You’re just using Go’s tools to make your code more expressive and modular.
And often, the code becomes easier to reason about — because each function now represents a specific intention instead of a bulky configuration.
Final Thoughts
Currying and partial application aren’t idiomatic everywhere in Go, but they’re incredibly useful in the right contexts. They help you build mini-APIs, encapsulate behavior, and avoid repetitive boilerplate — all while staying within Go’s simple mental model.
And honestly? They’re fun. They make your code feel lighter.


