Writing Go in 2025: No More if err != nil
A functional style for real-world Go code
View the go-fp repository on GitHub
Intro
In 2025, I don’t write Go like it’s 2015. I write Go functionally. No more if err != nil
scattered across my code. No more deeply nested error handling. Just monads.
I built a small functional programming package for Go: it gives me Lift
, LiftM
, Bind
, Map
, Then
, Match
, and many more both immutable chains and mutable wrappers. That’s all I need to write readable, composable, and safe Go code.
The Client Example
Full example on github
Here’s how a typical HTTP client looks using this approach:
func fetchUsers() immutable.Chain[[]User] {
urlChain := immutable.Wrap("http://localhost:8080/users")
respChain := immutable.Bind(urlChain, GetChain)
usersChain := immutable.Bind(respChain, parseUsers)
return usersChain
}
The functions being composed are also lifted. For example:
func GetChain(url string) immutable.Chain[*http.Response] {
return immutable.LiftResult(func() (*http.Response, error) {
return http.Get(url)
})
}
Chaining IO and JSON decoding becomes a matter of composition:
func parseUsers(resp *http.Response) immutable.Chain[[]User] {
defer resp.Body.Close()
bodyChain := immutable.Wrap(resp.Body)
dataChain := immutable.Bind(bodyChain, ReadAllChain)
usersChain := immutable.Bind(dataChain, UnmarshalChain[[]User])
return usersChain
}
In main()
, I process the chain:
usersChain := fetchUsers()
usersChain.
Then(func(users []User) ([]User, error) {
var filtered []User
for _, u := range users {
if u.Age > 25 {
filtered = append(filtered, u)
}
}
return filtered, nil
}).
Map(func(users []User) []User {
fmt.Println("Users older than 25:")
for _, u := range users {
fmt.Printf(" ID:%d Name:%s Age:%d\n", u.ID, u.Name, u.Age)
}
return users
}).
Match(nil, func(err error) {
log.Println("Error fetching users:", err)
})
How It Works
Wrap(val)
wraps a value into aChain
.Bind(chain, func)
composes functions that may return error.Then(func)
is for functions returning(T, error)
.Map(func)
is for pure transformations.Match(ok, err)
handles success and failure explicitly.
I started using this for everything: servers, clients, DB access, CLI tools. This is Go, but with a functional core. Just enough abstraction. Just enough control.