Like a one-line list comprehension to transform a collection is suddenly four lines of go: allocation, loop iteration, and append (don't even start me on the append function). I don't care about those housekeeping details. Let me read the business logic.
Modulo extremes like Java, the bottleneck for programmers understanding code is about semantics, not syntax -- effectively never the literal SLoC in source files. It's not as if
for i := range x {
x[i] = fn(x[i])
}
is any slower to read, or more difficult to parse, or whatever, than e.g. x.transform(fn)
in any meaningful sense.What's the go equivalent to
x.map(fn).filter(predicate)
ie returning a new collection of transformed items which is filtered by some predicate? Now we are talking more like 5-6 lines of Go.Probably something like
var output []T
for _, val := range input {
if newval := transform(val); allow(newval) {
output = append(output, newval)
}
}
No problem?For some operations, the Go style of explicit, mostly in-place mutations produces more complicated code. Whether that's balanced out by the code being "simpler" is not clear to me, but I haven't worked with Go.
I would not say it's obvious what the machine is doing in the Go example though. For example it wasn't clear to me that append() mostly doesn't copy the full vector, but does a copy of the slice pointer. I had to look it up from a blog post, because the source for append() is gnarly
https://github.com/golang/go/blob/go1.16.7/src/cmd/compile/i...
Well I guess you do have to grok the language spec and semantics in order to understand how builtins like append behave, I'm not sure that's avoidable.
x.transform(func(value int) string { return fmt.Sprintf("%b", value) })
This quickly becomes more difficult to read, especially if you want to chain some operations this way.But this applies to other languages as well, in JS (which has a function shorthand) I prefer to extract the predicates and give them a meaningful name.
between
var y []int
for _, x := range x {
y = append(y, function(x))
}
and let y = x.iter().map(function).collect();
I'll take the second form any day. You express the flow of information, and think about transformations to your collections.Go is not a high performance language which made a lot of decisions that don't lend its usage to be nice in scenarios where people want C and Rust. However, with the hype around it, the management continues to make decision, to everyone's detriment, to utilize Go in performance sensitive infrastructure code which one could write in Rust or C# and achieve much higher performance.
Go is all about mechanical sympathy, and is absolutely a high performance language. I guess it all depends on your context, though. If you're used to writing assembly or C, things may look different.
(A "for" loop expresses much more mechanical sympathy than a list comprehension, as an example.)
But at least in the context of application services -- programs that run on servers and respond to requests, typically over HTTP -- Go is the language to beat. I've yet to see an example of a program where the Rust implementation is meaningfully more performant than the Go implementation, and I've got plenty of examples where the Go implementation is much better.
However, it's a dangerous tool that some people just can't be trusted with. Second, if you go full FP style, then you can't just hire a Go developer, they need additional training to become productive.
Here's an example of functional programming within Go taken far: https://github.com/IBM/fp-go/blob/main/samples/http/http_tes.... It basically adds a DSL on top of Go, which goes against its principles of simplicity.
There was another great resource that explains why functional programming in Go is a Bad Idea; one is function syntax (there's no shorthand (yet?)), the other is performance (no tail call optimization), and another is Go's formatter will make it very convoluted; I think it was this one: https://www.jerf.org/iri/post/2955/