Second impressions of Go
Sep. 11th, 2015 09:57 pm![[personal profile]](https://www.dreamwidth.org/img/silk/identity/user.png)
Go is the new trendy programming language. In style it's kind of a backwards C, with an interesting amount of object-oriented features baked in. You can definitely get things done in Go, especially if you're not trying to interface to legacy systems.
As a modern language, though, the things it's missing seem odd. C++ has had parameterized template types as long as I've known it, and Java added them in eventually, but not Go, it's complicated. This means that basic functional-language primitives that are addictingly useful are essentially impossible to write. For instance, you can't call a function on every element of a slice (array view) without either specializing on the types or writing over and over:
But in a Real Language I'd just write:
Or even:
If I wrote a function G that converted ints to floats, in Go I need to write a totally new MapG() that has the exact same iterate-over-the-loop code. I can't find any good way to avoid the boilerplate without reflection.
Most things in Go work by returning pairs of an actual result and a flag or error object. This does lead to making it more obvious to try to do some error handling, and it is "better" than both exception-based languages (where it's easy to ignore errors until they crash your program) or C's magic return value (where an int is an int, unless it's -1). But it also leads to more boilerplate.
A lot of the code I'm writing seems to look like
In which three quarters of my code is boilerplate error checking. Haskell has a much-maligned Monad type class, but one extremely practical use of it is to pass along errors like this: it is able to encapsulate the repeated "if an error then produce an error, if not then pass the result of this to the next thing" block.
There's one other oddity I've run into: When is nil not nil? A Go "interface" value it turns out is a pair of a concrete type and a value, which means you can run into some counterintuitive behavior if the type is known.
Even though you've explicitly set the B field to nil, this prints out "a.B is not nil", because the actual return value is a pair of (type implB, value nil) which is different from (type nil, value nil). Instead you get to write
which feels...redundant.
Even so, the things that people find attractive about Go are still attractive. It's a compiled language, that isn't 30+ years old or owned by Oracle, that compiles reasonably obviously but can't obviously crash from pointer arithmetic errors. It's garbage-collected, which you may object to, but it beats the pants off of explicit memory management. The language includes maps and queues as base types, and if you secretly did like C memory management, you can relive the past with concrete arrays underneath slice views. I admit to having done almost nothing with goroutines, but the promise of the runtime having a select loop and thread management and synchronized channels in the core is much better than anything I've used that doesn't involve a big C library.
As a modern language, though, the things it's missing seem odd. C++ has had parameterized template types as long as I've known it, and Java added them in eventually, but not Go, it's complicated. This means that basic functional-language primitives that are addictingly useful are essentially impossible to write. For instance, you can't call a function on every element of a slice (array view) without either specializing on the types or writing over and over:
// F does the thing to the thing func F(x int) string { ... } // MapF does the thing to ALL THE THINGS! func MapF(xs []int) []string { result := make([]string, len(xs)) for i, x := range xs { result[i] = F(x) } return result }
But in a Real Language I'd just write:
-- f does the thing to the thing f :: Int -> String f x = ... -- mapF does the thing to ALL THE THINGS! mapF :: [Int] -> [String] mapF xs = map f xs -- ...but you'd just write "map f" in practice
Or even:
def f(x): ... def mapF(xs): """Do the thing to ALL THE THINGS!""" return [f(x) for x in xs]
If I wrote a function G that converted ints to floats, in Go I need to write a totally new MapG() that has the exact same iterate-over-the-loop code. I can't find any good way to avoid the boilerplate without reflection.
Most things in Go work by returning pairs of an actual result and a flag or error object. This does lead to making it more obvious to try to do some error handling, and it is "better" than both exception-based languages (where it's easy to ignore errors until they crash your program) or C's magic return value (where an int is an int, unless it's -1). But it also leads to more boilerplate.
A lot of the code I'm writing seems to look like
func A(x X) (Y, error) { ... } func B(y Y) (Z, error) { ... } func C(z Z) (string, error) { ...} func ABC(x X) (string, error) { y, err := A(x) if err != nil { return "", err } z, err := B(y) if err != nil { return "", err } return C(z) }
In which three quarters of my code is boilerplate error checking. Haskell has a much-maligned Monad type class, but one extremely practical use of it is to pass along errors like this: it is able to encapsulate the repeated "if an error then produce an error, if not then pass the result of this to the next thing" block.
a :: X -> Either String Y b :: Y -> Either String Z c :: Z -> Either String String abc :: X -> Either String String abc x = do y <- a x z <- b y c z main :: IO () main = let x = ... in case abc x of Left msg -> putStrLn ("Error: " ++ msg) Right result -> putStrLn ("Result: " ++ msg)
There's one other oddity I've run into: When is nil not nil? A Go "interface" value it turns out is a pair of a concrete type and a value, which means you can run into some counterintuitive behavior if the type is known.
package main import "fmt" type IntfA interface { B() IntfB } type IntfB interface { } type implA struct { theB *implB } func (a *implA) B() IntfB { return a.theB } type implB struct { } func main() { a := implA { theB: nil, } if a.B() == nil { fmt.Printf("a.B is nil\n") } else { fmt.Printf("a.B is not nil\n") } }
Even though you've explicitly set the B field to nil, this prints out "a.B is not nil", because the actual return value is a pair of (type implB, value nil) which is different from (type nil, value nil). Instead you get to write
func (a *implA) B() IntfB { if a.theB == nil { return nil } return a.theB }
which feels...redundant.
Even so, the things that people find attractive about Go are still attractive. It's a compiled language, that isn't 30+ years old or owned by Oracle, that compiles reasonably obviously but can't obviously crash from pointer arithmetic errors. It's garbage-collected, which you may object to, but it beats the pants off of explicit memory management. The language includes maps and queues as base types, and if you secretly did like C memory management, you can relive the past with concrete arrays underneath slice views. I admit to having done almost nothing with goroutines, but the promise of the runtime having a select loop and thread management and synchronized channels in the core is much better than anything I've used that doesn't involve a big C library.