Ten Reasons Why I Don't Like Golang

July 26, 2016

When I first started programming in Go, my summary of it was, “The good things are great and the bad things are weird and I can live with them.” After another three years and a few large projects in Go, I no longer like the language and wouldn’t use it for a new project. Here are 10 reasons why, in no particular order.

  1. Go uses capitalization to determine identifier visibility. Those that start with a lower case letter are package-private, and those that start with a capital are package-public. The purpose presumably is to cut down on the public and private keywords, but capitalization was already used to mean other things: Classes are capitalized and constants are entirely in upper case. It’s a recurring source of discomfort to me to name global constants entirely in lower case.

    Things get worse if you want to have a private struct, since it must be in lower case. For example you might have a user struct. What do you name the variable? Normally I’d call it user, but not only does that look confusing, but can cause parse errors when the compiler confuses them:

    type user struct {
        name string
    }
    
    func main() {
        var user *user
    
        user = &user{}    // Compile error
    }
    

    Idiomatic Go uses shorter names like u, but I stopped using one-letter variables 35 years ago when I left TRS-80 Basic behind.

    There are practical considerations, too. Frequently I start out with a private field or struct name and later decide to make it public, forcing me to fix all usage of the identifier. And even if you want to keep your fields private, you’re forced to make them public if you want to use the json package. In fact I had a struct with 74 private fields that I wanted to serialize with json and was forced to make all fields public and update all uses throughout the large app.

    Capitalization also restricts visiblity to two levels (package and completely public). I frequently want file-private identifiers for functions and constants, but there isn’t even a nice way for Go to introduce such a thing now.

  2. Structs do not explicitly declare which interfaces they implement. This is done implicitly by matching the method signatures. This design makes a fundamental error: It assumes that if two methods have the same signature, then they have the same contract. In Java when a class implements an interface, it’s doing more than telling the compiler that its method signatures will match. It’s also making a promise that the contracts of the methods were implemented. If the method returns a boolean, the comments in the interface will specify what the value means. (E.g., true on success, false on failure.)

    A Go struct might implement the same method signature but reverse the meaning of the return value. It’s free to do that – it never promised anything. The Java class can do this too, of course, but then it’s clearly a bug in the class. In Go the bug was introduced by whoever cast the object to the interface without first verifying the contractual compatibility of every method. This burden shouldn’t fall on every API user. It should be done once by the implementor of the struct and declared in the code.

  3. Go doesn’t have exceptions. It uses multiple return values to return errors. It’s far too easy to forget to check errors:

    db.Exec("DELTE FROM item WHERE id = 2")
    

    The DELETE is misspelled, and there will be nothing to tell you that something went wrong. If this is part of a large transaction, the whole transaction will silently do nothing. Good luck figuring out why. Error return values are fine but the programmer should be forced to check them (or assign the error to _).

    Additionally, the idiom of returning either a value or an error:

    user, err := getUserById(userId)
    

    invites bugs because there’s nothing to enforce the fact that exactly one of user and err contain valid values. With exceptions the user variable is never assigned-to (so reading from it will generate a warning), and with algebraic sum types (unions) the compiler will ensure that only the correct one can be accessed.

  4. There’s far too much magical behavior. For example, if I name my source file i_love_linux.go, it won’t get compiled on my Mac. If I accidentally name a function init() it’ll get run automatically. This is all part of the “convention over configuration” movement. It’s fine for small projects but bites you on large ones, and Go was meant to address the problem of “programming in the large”.

  5. Partly because of the capitalization problem, it’s easy to end up with several identically-named identifiers. It’s actually quite easy to have a package, struct, and variable all called item. In Java the package would be fully-qualified and the class would be capitalized. Sometimes I find it hard to read Go because I can’t always tell at a glance what scope an identifier belongs to.

  6. It’s difficult to generate Go code automatically. The compiler is strict about warnings, meaning that unused imports and variables cause the build to fail. But when generating a large file it may not be initially clear which packages need importing. Furthermore you may have two packages whose names clash, and it’s not easy to resolve this automatically because you can’t even know the imported symbol if you only know the package name. (The package imported as github.com/lkesteloot/foo is permitted to actually be package bar, and some open source libraries do this.) Even if you could figure that out, the generating program would be forced to alias the imports to avoid the conflict. In Java all these problems are solved by importing nothing and always fully-qualifying all class references, something not permitted in Go.

  7. There’s no ternary (?:) operator. Every C-like language has had this, and I miss it every day that I program in Go. The language is removing functional idioms right when everyone is agreeing that these are useful. Instead of the functional and elegant:

    var serializeType = showArchived ? model.SerializeAll : model.SerializeNonArchivedOnly
    

    you’re forced to this imperative verbosity:

    var serializeType model.SerializeType
    if showArchived {
        serializeType = model.SerializeAll
    } else {
        serializeType = model.SerializeNonArchivedOnly
    }
    

    I see no good argument for omitting this operator.

  8. The sort.Interface approach is clumsy. If you have 10 different structs that you want to sort (in arrays), you have to write 30 functions, and 20 of those are trivially similar (length and swap). Also, they’re hard to compose: You can maybe delegate to another Less(), but you have to trust that its Len() and Swap() are compatible. And finally it just looks weird, because the cast looks like a function call:

    sort.Sort(sort.Reverse(UsersByLastSignedInAt(users)))
    

    The tried and true approach of providing a compare method works great and has none of these drawbacks.

  9. Import versioning and vendoring is terrible. This is well-covered ground elsewhere, but frankly it’s 2016 and it’s not acceptable to release a new language without a solution for this. And not only does Go not have a solution, but its import system is actively hostile to vendoring.

  10. No generics. Also well covered elsewhere, but again I can’t really use a language that doesn’t let me implement a generic Stack class. The solution normally given is to open-code the stack using slice functions like append(), but again it’s 2016 and I want to write push() and pop(), not:

    stack = append(stack, object)
    

    and:

    object = stack[len(stack) - 1]
    stack = stack[:len(stack) - 1]
    

    I was surprised how many third-party libraries use interface{} throughout. This is a sign of a poorly-designed type system.

  11. Okay a bonus reason! This is very minor, but points to a failure on the part of the designers to understand how programmers work. The append() function extends an array, returning the new array:

    users = append(users, newUser)
    

    The problem is that the following code will nearly always work:

    append(users, newUser)
    

    The append() function modifies the array in-place when it can, and only returns a different array if it has no place left. You couldn’t ask for worse API design. How many bugs are caused by forgetting to assign the result? A lot, because initial testing may not trigger a resize. Either the construct should work differently (modifying the first argument in place) or should force the programmer to use the return value.

Here’s a summary of my recommendations for Go usage: If your program is small and can mostly be described by what it does, and if it doesn’t interact much with data outside itself (databases, the web), then Go is fine. If it’s large, if it has non-trivial data structures (even something simple like a tree), or if it will be dealing with a lot of data from the outside, then the type system will fight you for no benefit and you’re better off using a different static language (where the type system helps) or a dynamic language (where it doesn’t get in your way).