[personal profile] dmaze
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.
This account has disabled anonymous posting.
If you don't have an account you can create one now.
HTML doesn't work in the subject.
More info about formatting

If you are unable to use this captcha for any reason, please contact us by email at support@dreamwidth.org

Profile

dmaze

Expand Cut Tags

No cut tags
Page generated May. 24th, 2025 09:46 am
Powered by Dreamwidth Studios