Dear Santa, a Golang wish list
"If there's nothing you dislike about a programming language, you haven't used it long enough."
I love this pithy paraphrase because it so elegantly captures both the facts that 1) anything new and shiny often initially blinds us to its faults and that 2) there's no perfect programming language in existence, nor will there likely ever be one.
I'm professionally proficient in a handful of languages and capable enough to do some damage in a dozen more. Learning them one after another has reinforced the certitude that they are all just different tools, with their own unique uses, strengths, and weaknesses.
One of the languages I've picked up most recently, and since made my main tool for programming, is Go (aka Golang). It's a remarkable language!
At first, it was all rainbows and sunshine, as per above. Over the years, however, I've found one shortcoming after another, also as per above. Even Go is subject to the laws of reality and has its share of weaknesses to balance its strengths.
I maintain that this balance between strengths and weaknesses puts Go far ahead of the majority of languages out there. But that's the subject of another post. (Subscribe and I'll let you know when it's published!)
Go's missing features
There's an interesting paradox with Golang specifically. One of its weaknesses is its lack of features that you might miss from other languages. One of its strengths, on the other hand, is its ferocious dedication to maintaining a small feature set.
Features, however orthogonal, interact with each other. Their possible interactions grow exponentially with every new addition. In team management, this is known as Brooks' Law, after Fred Brooks and his excellent book, The Mythical Man-Month.
Where other languages seem to believe that more is always better, Go shows the power of intentional simplicity. There's a conscious attempt to stay closer to the triangle to the left than the mess to the right. This is part of what makes Go unique.
So it's with great reluctance and plenty of humility (plus some lolz) that I submit this list of features that I often miss when programming in Go.
1) List comprehension
A way to build lists based on existing lists (or slices, in the Go world). You'll find this in Python or Erlang and it could look something like this:
overfive := []int[x for x in allnumbers if x > 5]
Compare that with how you'd do it today:
overfive := []int{}
for _, x := range allnumbers {
if x > 5 {
overfive = append(overfive, x)
}
}
Some syntactic sugar to help make the code a little shorter (and quicker and easier to understand, I'd argue).
2) Ternary operator
This feature provides a simple way to express the choice between two values. You can find it heavily used in JavaScript and Ruby. How nice wouldn't it be to be able to do something simple like this?
greeting := user.Name != "" ? user.Informal() : user.Formal()
Idiomatic Go code today would instead look something like this:
var greeting string
if user.Name != "" {
greeting = user.Informal()
} else {
greeting = user.Format()
}
I'll admit I'm on the fence about this one in particular, as I've seen this feature abused to create some incomprehensible code. Still, I find myself missing it every now and again.
3) Interface properties
Interfaces and structs are still a bit weird for me, coming from more object-oriented languages. I've learned to love them, though!
Still, there's one feature I miss from languages like PHP and Java – abstract classes. It's the piece between an interface and a struct, which makes it possible to require a value object with not only methods but also properties.
Imagine if we could do this in Go:
type Plugin interface {
Do() error
Name string
}
There's no way to do that today. Either you'd have to rely on a specific implementation, using a struct:
type Plugin struct {
Name string
}
func (p Plugin) Do() error {
return nil
}
… or you'd have to mangle your beautiful design and use an interface like this:
type Plugin interface {
Do() error
Name() string
}
What's an extra pair of parenthesis, you ask me? Leave my magical fairyland alone, I respond!
4) Function guards
This is a given in many functional programming languages like Haskell and Scala. Function guards give us a way to clearly communicate edge cases and their handling, separate from the actual bulk of the function.
Consider the max()
method in the math package. It uses a switch statement for its guards.
func max(x, y float64) float64 {
// special cases
switch {
case IsInf(x, 1) || IsInf(y, 1):
return Inf(1)
case IsNaN(x) || IsNaN(y):
return NaN()
case x == 0 && x == y:
if Signbit(x) {
return y
}
return x
}
if x > y {
return x
}
return y
}
That's a lot of checks and balances before we get to the good parts. Let's rewrite that in an imaginary Go syntax, using pretend function guards. (And some cheeky ternary operators!)
func max(x, y float64) float64 {
return x > y ? x : y
}
?max() IsInf(x, 1) || IsInf(y, 1) {
return Inf(1)
}
?max() IsNaN(x) || IsNaN(y) {
return NaN()
}
?max() x == 0 && x == y {
return Signbit(x) ? return y : x
}
Yeah, okay, not the prettiest. I'm obviously not a language designer and functional languages usually have syntax better fitted for this kind of thing. But you get the point! Declaring the main logic separate from the exception cases makes things a tiny little bit nicer, in my humble opinion.
A better demonstration of first-class guards is the following example from Haskell, where we create a function that triples anything except the number 42.
triple :: Int -> Int
triple 42 = 42
triple x = x * 3
Of course, the Go team has already considered a similar feature called method overloading (albeit having a larger variety of applications). They came to the very reasonable conclusion not to include it in the language.
Bonus wishlist items
List comprehension, ternary operator, interface properties, and function guards. What a merry Christmas indeed! But wait, there's more.
I asked my esteemed colleagues at work for their wishlist items and they more than delivered. Thank you, Nicklas, Ola, and Bennie!
5) Type parameters in methods
Go 1.18 brought us generics, one of the most longed-for features in Go.
It's a nice tool but, to be honest, I don't get why people are SO crazy about it. It might not even have made my list, had I written this post a year ago. But, yeah, it comes in handy every now and again.
Now that we have it, though, there's one shortcoming that I keep bumping up again and again. And so it seems my colleague Nicklas does as well, as he gave me this:
Methods can't have type parameters.
Functions can! Like this one:
// A valid generic function.
func myfunction[T any](v T) {
fmt.Println(v)
}
Structs can too! And its methods can inherit its type:
// A valid generic struct.
type mystruct[T any] struct {}
// A method inheriting its struct's type.
func (_ mystruct[T]) mymethod(v T) {
fmt.Println(v)
}
But, alas, methods can't be generic. This, for example, won't compile:
type mystruct struct {}
// syntax error: method must have no type parameters
func (_ mystruct) mymethod[T any](v T) {
fmt.Println(v)
}
So close! At least from my userland perspective, completely ignorant of the mechanics behind what must be quite a hairy feature to implement and maintain.
6) Pointer assignment
How many times have you worked with pointers and wanted to define and assign a non-zero value in one go? It happens to me every now and again and so it seems to be for Ola, who added this to the wishlist.
I imagine the feature could look something like this in Go:
newint := &3
newstring := &"look ma, inline!"
Instead, today, we have to use one of two tactics:
// One: creating a locally scoped proxy.
newintv := 3
newint := &newintv
// Two: offloading that to a function.
func intp(i int) *int {
return &i
}
newint := intp(3)
There's the gox library and its NewInt(3)
function but wouldn't it be nice if it was built into the language?
EDIT: My friend Sebastian suggested using generics for this, which of course is the perfect use case for that feature:
func ptr[T any](t T) *T {
return &t
}
7) Tuples data type
One of the first things I noticed with Go was its multiple return values. I'd missed this feature for a long time, ever since I was programming Lua in early 2000.
On the basic level, it allows one construct (a function) to evaluate into a defined set of values. There are a lot of cases when this is useful but, in particular, it's become the backbone of Go's incredibly powerful (and equally simple) error handling.
func dosomething() (int, error) {}
value, error := dosomething()
If you're familiar with other languages like OCaml or C#, that last bit of the function signature looks exactly like a tuple – (int, error)
. Except, with Go, it's only available in return values.
I'd love to be able to use it as a native data type.
type logBuffer struct {
messages [](time.Time, string)
}
Some will argue that it makes the code clearer when you're forced to formally define a separate struct for those messages. I'll argue that it's still my wishlist and this addition would make me happy.
Merry Christmas!
There's a lot of candy coming out in the next 1.20 release but I doubt we'll see any of these wishlist features in Go anytime soon. And, to be honest, that's (mostly) a relief.
The Go team has made a great job keeping the language simple and explicit, which I believe to be two of its main competitive advantages. It would suck to see that dedication compromised, at least without huge benefits to balance it out.
So, Rob, Ken, Robert, Russ, and all the many others maintaining the Go programming language, here's for you: Thank you and Merry Christmas! 🎁 🎄 🙏