loading...

Why I’m So Frustrated With Go

#go
klnusbaum profile image Kurtis Nusbaum ・4 min read

It’s been about a year since I started using Go. I’ve written a lot of code with it, including an entire micro-service and a contribution to glide. I want to emphasize that, for the most part, it’s been pretty good. But about once a month, my eye starts twitching and I get the urge to start making a head-to-desk motion. The reasons for this can be pretty simply demonstrated. Since I seem to have this conversation on a regular basis, I thought I’d record my thoughts so I can just simply reference this the next time it comes up.

A Simple Data Structure

I need a data structure. It has the following requirements:

  1. It must store the name of an animal and an associated struct containing some information regarding the animal.
  2. It must have O(1) lookup time.
  3. I must be able to iterate over it’s elements in some predefined order.
  4. It must be immutable¹.

At first glance, we might be tempted to simply use a map like so:

type AnimalMap map[string]Animal

This is kinda nice. In fact, it satisfies the first two requirements of our data structure. But since the very early days of Go, the order objects are stored in a map is random (for good reason). So I can’t do something simple like insert all the elements into the map in the same order over which I’d like them to be iterated.

Alright, no big deal. We can get around this by just writing a little bit more code, using an array to keep track of order.

type AnimalMap struct {
  Mapping map[string]Animal
  Order []string
}

Now whenever we want to iterate over the map, we need to iterate over the Order slice instead, using the strings we pull off as keys into the map. Not the greatest, but it works.

What About Immutability?

This is where things get really hairy. Go doesn’t have the concept of runtime constants. If we want things to be immutable after we create them, we’re going to have to completely, and I mean completely, encapsulate both the slice and the map in our struct. This is because maps and slices have pass-by-reference semantics. In other words, if at any point I give you a reference to them, you’ll be able to modify them. The best I can come up with is now this explosion of code:

type Animals interface {
    Len() int
    At(i int) string
}

type animals []string
func (a animals) Len() int {return len(a)}
func (a animals) At(i int) string {return a[i]}

type AnimalMap struct {
    mapping map[string]Animal
    order   animals
}

func NewMap(order []string, mapping map[string]Animal) AnimalMap {
    newMap := AnimalMap{
        mapping: make(map[string]Animal, len(mapping)),
        order:   make(animals, len(order)),
    }

    for i, name := range order {
        newMap.mapping[name] = mapping[name]
        newMap.order[i] = name
    }

    return newMap
}

func (am AnimalMap) Order() Animals {return am.order}
func (am AnimalMap) Animal(name string) Animal {return am.mapping[name]}

What just happened? That’s an infuriating amount of code to write. You know how much code I’d have to write if this were C++, C#, or Java? None. They all have reusable notions of an immutable, ordered map. Yet in Go, every time I need one I have to write 30 lines of code. I feel like I had to fight to Go every step of the way here. I feel sad.

Conclusion

Go has some pretty neat concepts. I love that it enforces composition over inheritance. Goroutines are pretty nifty. I’ve been pretty satisfied with it overall. But time and again, I find myself solving the same problems over and over when using Go. I find myself completely blocked, with no way to write DRY code².

I’d love to see the Go community address issues like this. IMHO generics would solve all of the issues above, but I understand that’s somewhat of a sore topic for the Go community. Perhaps something like a simple form of codegen for common data structures and algorithms?


Kurtis Nusbaum is a Mobile and Backend Developer at Uber. He’s an avid long distance runner and diversity advocate. Here's his mailing list. Here's his Twitter, and LinkedIn. Here’s his github. If Kurtis seems like the kind of guy with whom you’d like to work, shoot him an e-mail at kcommiter@gmail.com.


¹Here's why you might want immutability
²For instance, try writing a reusable exponential backoff algorithm or a reusable binary tree. Oh, and you can’t use interface{}. We’re programming in a statically typed language for a reason.

Discussion

pic
Editor guide
Collapse
nickgrim profile image
Nick Grimshaw

To solve the iterate-in-some-order-but-maintain-immutability problem, how about something like this:

type AnimalMap struct {
        mapping map[string]Animal
        order []string
}

func (am *AnimalMap) Range(f func(Animal)) {
        for _, s := range am.order {
                f(am.mapping[s])
        }
}

...and then just:

printThem := func(a Animal) {
        fmt.Printf("%v\n", a)
}
m.Range(printThem)
Collapse
klnusbaum profile image
Kurtis Nusbaum Author

I hadn't thought about this. This is interesting.

That said, I think the boiler plate of always having to wrap my code in a closure is a little frustrating. The example provided here is not to bad, but I think I could envision code that's more complex and this being a little more frustrating to do. Not only that, but I have to wrap each new piece of iteration code with a closure.

What do you think?

Collapse
klnusbaum profile image
Kurtis Nusbaum Author

I thought about this a little more and I really like your solution. While it adds closure boiler plate at the call site, it also removes the need for the caller to write their own loop. So it's actually a wash in that sense. But it removes all the stuff I had to add for immutability. I actually really like this. My hats off to you.

That said, it's still 10 lines of code I have to write every time I want a data structure like this. Which is an order of magnitude larger than other languages.

Thread Thread
irongopher profile image
Darren Venables

/u/shovelpost commented this on reddit.

"TL;DR

  • "Author complains he has to write 30 extra lines of code in Go.
  • "Person in comments provides a solution that reduces code to 10 lines.
  • "Author complains he has to write 10 extra lines of code in Go."

You are a developer, and you must write some code. Go is not a language meant to be all things to all people. Look at C++, C#, and Java to see what happens to languages that walk that road: they become bloated with features, fast.

Thread Thread
chicocode profile image
Francisco Hisashi Berrocal

In Javascript you don't need 10 lines of code to accomplish the same results and is still a small core language.
You would use some lib to do that, maybe the problem with go is the immature ecosystem.

Collapse
blixt profile image
Blixt

Why not make the map an index into a slice of Animal if you're already going for immutability? Simpler/faster to iterate.

type AnimalMap struct {
    index map[string]int
    items []Animal
}

(The key lookup would become am.items[am.index["value"]])

If you were going for mutability, it'd be simpler to make the index map[string]*Animal (which might not be a bad idea even with immutable access – of course the index matters more/less depending on how much you'd be inserting/deleting.).

Collapse
rkuris profile image
Ron Kuris

I wish I understood why immutable structures are so important, unless you are wanting a functional programming paradigm, which leaves only Haskell or Scala. Write me something in another language that I can't mutate.

IMHO if you want something immutable, don't mutate it.

Collapse
_andys8 profile image
Andy

"... don't mutate it": But who verifies you didn't? Your tooling should. Think about tests, formatting and linting. People could just write perfect code and not use these tools either ;)

Immutability leads to better structured code if people think about which values actually change, which not and how they depend on each other.

Collapse
klnusbaum profile image
Collapse
rkuris profile image
Ron Kuris

I think even these discussions miss a key point. I can't tell you how many times I've had, in Java, someone just throw "final" on their class because they thought nobody should extend it. I've had to copy entire classes because I couldn't use something from a library more times than I can count just for this reason.

Even "String" in java is final and immutable. I want mutable strings, for all the reasons mentioned in many of these posts -- much faster, easier to deal with, etc. Because of someone's idea that all strings should be immutable, I end out constructing tons of objects that I don't need, and compiler writers spend thousands of hours improving the optimizer to avoid these allocations, and programmers resorting to libraries with buffers and such in order to add simple strings together, all because someone thought string should be immutable.

Thread Thread
jimleroyer profile image
jlr

I also don't like the habit of certain programmers to define all of their Java classes as final, but that is not immutability of object instances that the author refers to (preventing class extension vs preventing modification of objects at runtime). I think it's a valid rant but that sounds like a different topic.

Regarding mutable Strings in Java, I wonder why you had to use libraries. There are StringBuffer and StringBuilder just for that in core Java (former is present since JDK1.0), sitting alongside String (in java.lang package).

On the topic of immutability and specifically String, James Gosling gave an explanation back in 2001: artima.com/intv/gosling313.html

Performance (there are cases where immutability can provide more performance) and security basically were his two main reasons for making String immutable. I particularly like this quote:

You end up getting almost forced to replicate the object because you don't know whether or not you get to own it.

That's obvious for C, C++ or/and Rust programmers. Maybe less when we are used to work with a VM that does most of the memory allocation and ownership management.

Collapse
klnusbaum profile image
Kurtis Nusbaum Author

Russ Cox has also outline why he thinks immutability is a good idea in his 2017 resolutions: research.swtch.com/go2017

(he also says he wants to a get a better understanding of generics :)

Collapse
ikirker profile image
Ian Kirker

For instance, try writing a reusable exponential backoff algorithm. Oh, and you can’t use interface{}.

"For instance, try writing a matrix/matrix multiply. Oh, and you can't use numerical types. We're writing in a language with string support for a reason."

It's not an exact analogy, but it seems disingenuous to include an arbitrary handicap and then complain that something is difficult.

Collapse
klnusbaum profile image
Kurtis Nusbaum Author

I would argue that the handicap is not arbitrary. As I explained in the sentence after, using interface{} would cause the loss of static type checking which is incredibly valuable.

Collapse
tynor profile image
Tynor Fujimoto

For instance, try writing a reusable exponential backoff algorithm […] Oh, and you can’t use interface{}.

How about something like this?

func ExponentialBackoff(tries int, unit time.Duration, f func() error) error {
    var err error
    for i := 0; i < tries; i++ {
        err = f()
        if err == nil {
            return nil
        }
        top := int32(math.Exp2(float64(i))) + 2
        n := rand.Int31n(top)
        time.Sleep(time.Duration(n) * unit)
    }
    return err
}

Of course it could be simplified to remove the unit argument if you knew ahead of time which unit you wanted to use.

It could be used like so:

n := 0
f := func() error {
    if n > 5 {
        return nil
    }
    n++
    return errors.New("failed")
}
ExponentialBackoff(10, time.Millisecond, f)
Collapse
joncalhoun profile image
Jon Calhoun

While not perfect, I talk about using code generation for problems like this here: dev.to/joncalhoun/using-code-gener...

And then there are a few pretty solid generic generation libs out there if you find them more useful. github.com/cheekybits/genny being one of them.

Not as nice as generics but they help.

Collapse
pmarreck profile image
Peter Marreck

Disclaimer: I am no fan of Go (currently quite enamored with Elixir)

Lacking some OrderedMap builtin, couldn't someone simply write a library to give everyone in Go this functionality?

Anyway, what you're experiencing is exactly why I left OOP langs: You can use a functional immutable style all you want in any OO lang, but as soon as you depend on the language ecosystem and libraries, all guarantees and most benefits go out the window IMHO.

Collapse
timuckun profile image
Tim Uckun

Try Crystal.

Collapse
ryanjhayden profile image
ryanjhayden

I could understand the frustration or having to implement your own filter logic. I ran into the same issue before, though I didn't have to worry about the slice being immutable.

Maybe something like this in the future?
github.com/ahmetb/go-linq

Collapse
bgadrian profile image
Adrian B.G.

I see your point, but "A Simple Data Structure" with "4 not so simple requirements", I can`t blame Go, it is a simple language. From what I saw so far the lack of generics is a big issue.

Collapse
szdavid92 profile image
Dávid Szakállas

I wouldn't say Java or C# supports immutability. These languages have non-transparent reference types and there is no way to enforce top-to-bottom immutability. You can e.g get an item from an immutable map and change one of its properties. However I understand when you, or other people refer to an immutable map in a weak sense meaning that mappings can't change. The reason Go falls short here is it's 'dumb' type system making it boilerplatey for the developer and unfriendly (casting!) for the consumer to add in your case an immutable map (generally any custom collection type) stemming from the design choice to omit generics and restrict you to use Go's predefined collection types instead. If you want a language with a more extensible type system, use any of the ones you already mentioned: C++/C#/Java. I don't think this will change in the near future, as it was very clearly declared that Go chose this path. Moreover, if you want top-to-bottom immutability you need to surrender and use a functional language, especially that nested persistent structures in statically typed languages need lenses or such to handle concisely, and requiring more complicated type system Go or for that matter C# or Java will ever have.

Collapse
nobozo profile image
Jon Forrest

Minor typo:

"I must be able to iterate over it’s elements" ->
"I must be able to iterate over its elements"