Go and multiple return values
I recently saw a post on Lobste.rs titled “Were multiple return values Go’s mistake?”.
I disagree with the author’s conclusion. They raise interesting points, especially regarding concurrency and some unexpected behaviors, to people new to Go.
The author starts with a basic example:
func foo() (int, error) {
return 123, nil
}
They suggest this looks like a tuple. And then base the entire argument on that idea.
To me, it simply looks like returned values. Go’s core strength lies in functions and data structures—chunks of data in memory. Functions operate on this data: you pass in data, and you get data back. This feels natural. When your tools are limited to data and functions, you can only do things with data and functions. You avoid complex architectures by design. You solve problems with functions and data structures. Coming from other languages, you might be used to “architectural patterns”, “best practices”, layers of complexity.
And while those patterns solve complex problems, with Go you take a different approach. You keep writing functions and data structures. If you need something more complex, you create a new data structure and a new function. (I mean, you can, and there are some ideas, but most are simple and straightforward and you don’t need a book to understand them.)
Anyway, back to “multiple return values are a mistake”. The argument that you can’t “just pass data around without additional ceremony” is illustrated with this example:
func doStuff() (string, error) { ... }
func doSomeStuff() {
var results [](string, error)
for range 10 {
results = append(results, doStuff())
}
}
I don’t know, let’s imagine doStuff
makes an API request, returns result or error.
The author argues, “This looks like a tuple, so let’s do tuple operations.” I think that’s their core misunderstanding—it’s not a tuple; it’s two separate values. You can’t create a slice of two values.
If you need such a data structure, create one:
var results []struct {
str string
err error
}
You still need to assign the return values of doStuff to variables and then append a struct. But if you need a struct, why not use it directly?
type Something struct {
str string
err error
}
func doStuff() Something { ... }
// now you can append your thing
results = append(results, doStuff())
Now it’s a thing you can put into a slice. While other languages have tuples and syntactic sugar, Go returns values, not tuples. If you need a different data structure, return it, or create a struct in place.
The author attempts to treat these return values as tuples in concurrency examples (using channels), but channels, like slices accept only one data structure type. Again, use a struct if you need something else. This isn’t a workaround; it’s working with the data structures you need. The author’s complaints focus on Go’s lack of tuples, not on multiple return values. Returning multiple values feels natural in Go.
This “functions and data structures” approach promotes simplicity. You avoid complex architectures. You don’t get fancy. Because you don’t need to. People used to complex software with deep abstractions might not appreciate Go’s simplicity. Go can get verbose sometimes, that’s true. But you can still understand everything, every file you open.
Maybe that’s the problem. Maybe I’m just stupid, and can’t see the value in the complex architectures and abstraction layers. I can understand a lot of it, but I don’t like them. I see them as obscuring the problem being solved. If you want to think about a problem, think of it as a mathematical problem. A few values. Some functions. That’s it. And yes, maths can get incredibly crazy, abstract and amazing. But it’s still based on simple assumptions and builds on them. It doesn’t invent any magic to “hide complexity”.
Perhaps I’m missing something, but I prefer solving problems over writing complex software. Go excels at this. It’s direct, straightforward, and lacks magic. Just functions and data structures.