Previous posts Why Go is not my favourite language and Go programs are not portable have me critiquing Go for over a decade.

These things about Go are bugging me more and more. Mostly because they’re so unnecessary. The world knew better, and yet Go was created the way it was.

For readers of previous posts you’ll find some things repeated here. Sorry about that.

Error variable scope is forced to be wrong

Here’s an example of the language forcing you to do the wrong thing. It’s very helpful for the reader of code (and code is read more often than it’s written), to minimize the scope of a variable. If by mere syntax you can tell the reader that a variable is just used in these two lines, then that’s a good thing.

Example:

if err := foo(); err != nil {
   return err
}

(enough has been said about this verbose repeated boilerplate that I don’t have to. I also don’t particularly care)

So that’s fine. The reader knows err is here and only here.

But then you encounter this:

bar, err := foo()
if err != nil {
  return err
}
if err = foo2(); err != nil {
  return err
}
[ a lot of code below ]

Wait, what? Why is err reused for foo2()? Is there’s something subtle I’m not seeing? Even if we change that to :=, we’re left to wonder why err is in scope for (potentially) the rest of the function. Why? Is it read later?

Especially when looking for bugs, an experienced coder will see these things and slow down, because here be dragons. Ok, now I’ve wasted a couple of seconds on the red herring of reusing err for foo2().

Is a bug perhaps that the function ends with this?

// Return foo99() error. (oops, that's not what we're doing)
foo99()
return err // This is `err` from way up there in the foo() call.

Why does the scope of err extend way beyond where it’s relevant?

The code would have been so much easier to read if only err’s scope had been smaller. But that’s not syntactically possible in Go.

This was not thought through. Deciding on this was not thinking, it was typing.

Two types of nil

Look at this nonsense:

package main
import "fmt"
type I interface{}
type S struct{}
func main() {
    var i I
    var s *S
    fmt.Println(s, i) // nil nil
    fmt.Println(s == nil, i == nil, s == i) // t,t,f: They're equal, but they're not.
    i = s
    fmt.Println(s, i) // nil nil
    fmt.Println(s == nil, i == nil, s == i) // t,f,t: They are not equal, but they are.
}

Go was not satisfied with one billion dollar mistake, so they decided to have two flavors of NULL.

“What color is your nil?” — The two billion dollar mistake.

The reason for the difference boils down to again, not thinking, just typing.

It’s not portable

Adding comment near the top of the file for conditional compilation must be the dumbest thing ever. Anybody who’s actually tried to maintain a portable program will tell you this will only cause suffering.

It’s an Aristotle way of the science of designing a language; lock yourself up in a room, and never test your hypotheses against reality.

The problem is that this is not year 350 BCE. We actually have experience that aside from air resistance, heavy and light objects actually fall at the same speed. And we have experience with portable programs, and would not do something this dumb.

If this had been the year 350 BCE, then this could be forgiven. Science as we know it hadn’t been invented yet. But this is after decades of very widely available experience in portability.

More details in this post.

append with no defined ownership

What does this print?

package main
import "fmt"
func foo(a []string) {
    a = append(a, "NIGHTMARE")
}
func main() {
    a := []string{"hello", "world", "!"}
    foo(a[:1])
    fmt.Println(a)
}

Probably [hello NIGHTMARE !]. Who wants that? Nobody wants that.

Ok, how about this?

package main
import "fmt"
func foo(a []string) {
    a = append(a, "BACON", "THIS", "SHOULD", "WORK")
}
func main() {
    a := []string{"hello", "world", "!"}
    foo(a[:1])
    fmt.Println(a)
}

If you guessed [hello world !], then you know more than anybody should have to know about quirks of a stupid programming language.

defer is dumb

Even in a GC language, sometimes you just can’t wait to destroy a resource. It really does need to run as we leave the local code, be it by normal return, or via an exception (aka panic).

What we clearly want is RAII, or something like it.

Java has it:

try (MyResource r = new MyResource()) {
  /*
  work with resource r, which will be cleaned up when the scope ends via
  .close(), not merely when the GC feels like it.
  */
}

Python has it. Though Python is almost entirely refcounted, so one can pretty much rely on the __del__ finalizer being called. But if it’s important, then there’s the with syntax.

with MyResource() as res:
  # some code. At end of the block __exit__ will be called on res.

Go? Go makes you go read the manual and see if this particular resource needs to have a defer function called on it, and which one.

foo, err := myResource()
if err != nil {
  return err
}
defer foo.Close()

This is so dumb. Some resources need a defer destroy. Some don’t. Which ones? Good fucking luck.

And you also regularly end up with stuff like this monstrosity:

f, err := openFile()
if err != nil {
  return nil, err
}
defer f.Close()
if err := f.Write(something()); err != nil {
  return nil, err
}
if err := f.Close(); err != nil {
  return nil, err
}

Yes, this is what you NEED to do to safely write something to a file in Go.

What’s this, a second Close()? Oh yeah, of course that’s needed. Is it even safe to double-close, or does my defer need to check for that? It happens to be safe on os.File, but on other things: WHO KNOWS?!

The standard library swallows exceptions, so all hope is lost

(Largely a repeat of part of a previous post)

Go says it doesn’t have exceptions. Go makes it extremely awkward to use exceptions, because they want to punish programmers who use them.

Ok, fine so far.

But all Go programmers must still write exception safe code. Because while they don’t use exceptions, other code will. Things will panic.

So you need, not should, NEED, to write code like:

func (f *Foo) foo() {
    f.mutex.Lock()
    defer f.mutex.Unlock()
    f.bar()
}

What is this stupid middle endian system? That’s dumb just like putting the day in the middle of a date is dumb. MMDDYY, honestly? (separate rant)

But panic will terminate the program, they say, so why do you care if you unlock a mutex five milliseconds before it exits anyway?

Because what if something swallows that exception and carries on as normal, and you’re now stuck with a locked mutex?

But surely nobody would do that? Reasonable and strict coding standards would surely prevent it, under penalty of being fired?

The standard library does that. fmt.Print when calling .String(), and the standard library HTTP server does that, for exceptions in the HTTP handlers.

All hope is lost. You MUST write exception safe code. But you can’t use exceptions. You can only have the downsides of exceptions be thrust upon you.

Don’t let them gaslight you.

Sometimes things aren’t UTF-8

If you stuff random binary data into a string, Go just steams along, as described in this post.

Over the decades I have lost data to tools skipping non-UTF-8 filenames. I should not be blamed for having files that were named before UTF-8 existed.

Well… I had them. They’re gone now. They were silently skipped in a backup/restore.

Go wants you to continue losing data. Or at least, when you lose data, it’ll say “well, what (encoding) was the data wearing?”.

Or how about you just do something more thought through, when you design a language? How about doing the right thing, instead of the obviously wrong simple thing?

Memory use

Why do I care about memory use? RAM is cheap. Much cheaper than the time it takes to read this blog post. I care because my service runs on a cloud instance where you actually pay for RAM. Or you run containers, and you want to run a thousand of them on the same machine. Your data may fit in RAM, but it’s still expensive if you have to give your thousand containers 4TiB of RAM instead of 1TiB.

You can manually trigger a GC run with runtime.GC(), but “oh no don’t do that”, they say, “it’ll run when it has to, just trust it”.

Yeah, 90% of the time, that works every time. But then it doesn’t.

I rewrote some stuff in another language because over time the Go version would use more and more memory.

It didn’t have to be this way

We knew better. This was not the COBOL debate over whether to use symbols or English words.

And it’s not like when we didn’t know at the time that Java’s ideas were bad, because we did know Go’s ideas were bad.

We already knew better than Go, and yet now we’re stuck with bad Go codebases.

Other people’s posts

  • https://www.uber.com/en-GB/blog/data-race-patterns-in-go/
  • https://fasterthanli.me/articles/lies-we-tell-ourselves-to-keep-using-golang
  • https://fasterthanli.me/articles/i-want-off-mr-golangs-wild-ride