dmaze ([personal profile] dmaze) wrote2015-09-11 09:57 pm
Entry tags:

Second impressions of Go

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:

// 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.