Ian Byrd


Go is a poorly designed language

TL;DR: All in all, I’ll continue to use Go. For a good reason: just because I love it. I hate the language: it’s absolute crap, but I love community, I love tooling, I love fancy design decisions (hey interfaces) and the whole ecosystem.

Hey man, wanna fork Go a litle bit?


Alright, the title is quite bold, I admit it. I’ll tell you more: I love bold titles! In this blog post I’ll try to prove that Go is a terribly designed language (spoiler: it’s not.) I’d been playing with Go for a couple months already and run my first helloworld somewhen in June, I think. I am not particularly good at math, but it’s already been four months since then and got a few packages on GitHub already. Needless to say, I also had absolutely no experience of using Go in production, so in case if I do talk about it “code support”, “deployment” and related… make sure you take it with a reasonable grain of salt.

I love Go. I fell in love with it ever since I had tried it for the first time. It took me a few days-weeks to accept idiomatics and overcome lack of generics, keep it up with the weird error handling and all the classic Go issues. I read Effective Go, many articles on Dave Cheney’s blog, kept track of everything related to Go and so on. I’d say I instantly became a pretty active community member! I love Go and I just can’t help it—Go is amazing. Still, in my humble opinion, Go is a terribly poorly designed language, which does exactly the opposite to what it advertises.

Go is considered a simple programming language. According to Rob Pike, they took everything out of the language, making its spec as much trivial as possible. This side of the language is amazing: you can learn the basics in the matter of hours, get into the real coding straight away and it’d work: in most cases Go works just as expected. You’d be pissed off, but it’s here, working. Reality is quite different tho: Go is not a simple language (well it is but still), it’s just poor. Here are some points proving it.

Slice manipulations are broken

Slices are great, I really like the concept and some of the implementation. But let’s for a whole second, imagine, that we might actually want to write some source code with them. Obviously, slices live in the heart of the language, they are what makes Go great. But again, let’s imagine that just occasionally, in between “concept” talks, we’d want to write some real code. The following code listing is how you do slice manipulations in Go:

numbers := []int{1, 2, 3, 4, 5}

log(numbers)         // 1. [1 2 3 4 5]
log(numbers[2:])     // 2. [3 4 5]
log(numbers[1:3])    // 3. [2 3]

// numbers[:-1] from Python or numbers[:$-1] from D?
// Nah, not the Go way.
//
log(numbers[:len(numbers)-1])    // 4. [1 2 3 4]

// “Terrific” readability, Mr. Pike! Well done!
//
// Now let’s say, I want to append six:
//
numbers = append(numbers, 6)

log(numbers) // 5. [1 2 3 4 5 6]

// Remove number 3 from the slice:
//
numbers = append(numbers[:2], numbers[3:]...)

log(numbers)    // 6. [1 2 4 5 6]

// Wanna insert some number? Don’t worry, there is
// a common idiomatic best practice in Go!
//
// I particularly love the triple-dotty thing, haha.
//
numbers = append(numbers[:2], append([]int{3}, numbers[2:]...)...)

log(numbers)    // 7. [1 2 3 4 5 6]

// Here is what you do in order to copy it:
//
copiedNumbers := make([]int, len(numbers))
copy(copiedNumbers, numbers)

log(copiedNumbers)    // 8. [1 2 3 4 5 6]

// And there’s more.

Believe it or not, that’s how Go programmers do really transform slices. We don’t have to do this everyday, but once we do—it’s fun time. And we don’t have any sort of generics (haha lmao), so you can’t create a pretty insert() function that would potentially hide this horror.

Sometimes nil interfaces are not entirely nil

They tell us “errors in Go are more than strings” and that you shouldn’t treat them as strings. For example, spf13 from Docker said so on his adorable “7 common mistakes in Go and when to avoid them” talk.

They also tell us that we ought to return error interface instead of specific errors (consistency, readability, etc). That’s what I’m gonna do in the following code listing. You’d be surprised, but this program will indeed say hello to Mr. Pike. But wait, is it really expected to?

package main

import "fmt"

type MagicError struct{}

func (MagicError) Error() string {
	return "[Magic]"
}

func Generate() *MagicError {
	return nil
}

func Test() error {
	return Generate()
}

func main() {
	if Test() != nil {
		fmt.Println("Hello, Mr. Pike!")
	}
}

Yeah, I am aware of why this occurs, since after a while around you realize how interfaces do really work and eventually wrap your heard around it. But for a newcomer.. come on, guys, it is a hammer! As you can see, Go is such a straight-forward and easy-to-learn language with all the distractive features taken out, it obviously lets nil interfaces to be not entirely nil, just for your own debugging lulz! That said, this issue is covered in the official FAQ section:

An interface value is nil only if the inner value and type are both unset, (nil, nil). In particular, a nil interface will always hold a nil type. If we store a nil pointer of type *int inside an interface value, the inner type will be *int regardless of the value of the pointer: (*int, nil). Such an interface value will therefore be non-nil even when the pointer inside is nil.

This situation can be confusing, and arises when a nil value is stored inside an interface value such as an error return:

As you can see, according to Go team, this is not an issue, but an ordinary confusing situation, which is obviously just fine. Go is a straight-forward and easy-to-learn language with all the distractive features taken out.. including non-confusing interface implementation!

Funny variable shadowing

Just in case you are not familiar with the term, let me quote Wikipedia here: “variable shadowing occurs when a variable declared within a certain scope (decision block, method, or inner class) has the same name as a variable declared in an outer scope.” Seems legit, quite a common practice, most languages support variable shadowing nowadays. Go is not an exception, yet it’s different. That’s how shadowing works here:

package main

import "fmt"

func Secret() (int, error) {
	return 42, nil
}

func main() {
	number := 0

	fmt.Println("before", number) // 0

	{
		// meet the shadowing
		number, err := Secret()
		if err != nil {
			panic(err)
		}

		fmt.Println("inside", number) // 42
	}

	fmt.Println("after", number) // 0
}

Yeah, I’m aware that := operator creates a new variable and assigns it a corresponding right-hand value, so according to the language spec it’s absolutely legit. But here is a funny thing: try removing the inner scope—it’d work just as expected (“after 42”). Otherwise, say hello to variable shadowing. That said, this sort of shadowing comes handy sometimes: you see, error handling in Go is done by returning error interface as the second return value. There is absolutely no elegant way to let this without introducing this sort of a funky shadowing circus.

Needless to say, it’s not just some funny example I came up with before breakfast, it’s a real issue everybody runs into eventually. I’ve been refactoring some Go code earlier this week and run into it twice. Compiler’s fine, linters are fine, everybody is fine, except the code.

No first-class support for interfaces

Pike&Co. keeps on saying that interfaces are basically what Go makes Go itself: interfaces is how you work around generics, it’s how you do mock testing, it’s the way polymorphism’s implemented. Usually, interfaces are the answer to all your pains. Believe it or not, I loved the interfaces with my heart right off reading “Effective Go” and I’ve kept on loving them ever since. Although except the “this nil interface is not nil sometimes” issue I addressed above, there is an another nasty thing which make me think that interfaces do not have a first-class support in Go. Basically, you can’t pass a slice of structs (items of which satisfy some interface) to a function, recieving a slice of this interface type:

package main

import (
	"fmt"
	"strconv"
)

type FancyInt int

func (x FancyInt) String() string {
	return strconv.Itoa(int(x))
}

type FancyRune rune

func (x FancyRune) String() string {
	return string(x)
}

// Literally any object with String() method
type Stringy interface {
	String() string
}

// String, made of string representations of items given.
func Join(items []Stringy) (joined string) {
	for _, item := range items {
		joined += item.String()
	}

	return
}

func main() {
	numbers := []FancyInt{1, 2, 3, 4, 5}
	runes := []FancyRune{'a', 'b', 'c'}

	// You can't do this!
	//
	// fmt.Println(Join(numbers))
	// fmt.Println(Join(runes))
	//
	// prog.go:40: cannot use numbers (type []FancyInt) as type []Stringy in argument to Join
	// prog.go:41: cannot use runes (type []FancyRune) as type []Stringy in argument to Join
	//
	// Instead, you are supposed to do this:
	//

	properNumbers := make([]Stringy, len(numbers))
	for i, number := range numbers {
		properNumbers[i] = number
	}

	properRunes := make([]Stringy, len(runes))
	for i, r := range runes {
		properRunes[i] = r
	}

	fmt.Println(Join(properNumbers))
	fmt.Println(Join(properRunes))
}

Unsurprisingly, this is a known issue, which is not considered an issue at all. It’s just a yet another funny thing about Go, alright? I really recommend you to read a related wiki on point, you’d find out why the “passing struct slice as interface slice” won’t work. But hey, just think about it! Spoiler: under the hood, interfaces and structs have a diffeent memory layout; slice is a continuos memory junk of fixed capacity, you can’t do anything about it. Yet we can actually implement the desired behaviour, there is no magic, it’s just a compiler issue. Look over there, I just did an explicit conversion from []struct to []interface! It does allocation though. Why can’t Go compiler do this for me? If it could, it’d likely do it without any (by generating type-specific functions). Yeah, explicit is better than implicit, but wtf, Go?

I just can’t stand how people look at this sort of rubbish, language is full of and keep on saying “yeah, it’s *just fine*”. It is not. It’s what makes Go a horribly designed language.

Questionable compiler rigidity

As I could have told you before, Go is considered clear, simple and readable language with a rather strict compiler. For instance, you can’t compile a program with unused imports. Why? Just becoz Mr. Pike and his friends think it’s right. Unused import is not the end of the world: you most certainly can live with it just fine. I totally agree that it’s not right and compiler must print a related warning, but why the hell would you terminate compilation because of such a minor issue? Unused import, really? Yeah, it does make overall compilations slower, but do we really care about this while doing intensive coding iterations? We obviously could have a fancy warning (which you could turn off) and pre-commit hook that automatically removes all unused imports instead. Unfortunately, it’s just fine.

Go1.5 introduced a funny language change: now you may list map literal omitting the contained type name listed explicitly. Think about it: it took them five (or even more) years to realize that putting value type in map initialization list all over the place might be excessive. In fact, I’m real surprised they actually made it!

Another thing in Go I particularly enjoy is “readability” thing: commas. You see, in Go you can define multiline import, const and var blocks: import (

    "fmt"
    "math"
    "github.com/some_guy/fancy"
)
const (
    One int = iota
    Two
    Three
)
var (
    VarName int = 35
)

Alright, it’s just fine. But once it comes down to “readability”, Rob Pike decided that it might be great to add some commas. At some point, after adding commas, he’s decided you should keep the trailing comma as well! So instead of writing this:

numbers := []Object{
    Object{"bla bla", 42}
    Object("hahauha", 69}
}

You must write this:

numbers := []Object{
    Object{"bla bla", 42},
    Object("hahauha", 69},
}

It’s not a huge issue, tbh. It does irritate people from time to time, usually on big struct init scope tho. I still wonder why we can omit commas on import/var/const blocks and just can’t on lists and maps. Anyway, Rob Pike knows better than me! Viva la Readability!

Go generate is a quirk

First of all, note that I’m not against code generation. For a language with Go sort-of poor type system, it might be the only viable way of omitting some copy-pasting for all the generic type of stuff. Still, go:generate—a code generation tool from the default Go shed is just silly. Well, to be fair, the tool itself is fine, I like it. It’s the whole approach which is just wrong. In order to generate some code you’re supposed to use some special magic comment. Yeah, some magic sequence of bytes somewhere in the code comments leads to code generation.

The thing is. Comments are meant to explain code, not generate it. Magical commenting is a thing in the modern Go tho. Interestingly, nobody actually cares, since it’s obviously just fine (you wonder!). In my humble opinion, it’s definitely much-much worse than the freaking imports.

Epilogue

As you can see, I didn’t complain about generics, error handling, syntactic sugar and/or other quite classical Go-related issues. I agree that generics are not critical, but if you take away generics, please give us some normal code generation tools or make a fancier compiler, not some random punky funky magical comment bollox. If you take away exceptions, please give us an ability to safely compare interfaces to nil, without wondering whether it derived from another type or not. If you take away syntactic sugar, please let us write the code that works as expected, without some “confusing” interface magic.

All in all, I’ll continue using Go for good. I hate the language and find it pretty bland even for the problems it solves, but I love the community (with a slight exception of “golang bridge” comm or whatever they call themselves, which I had horrible experience with) and the tooling.

Hey man, wanna fork Go a little bit?