Why I Don’t Like Golang (2016)

admin2 admin2
12 Min Read

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.

  • 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.

  • 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.

  • 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.

  • 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”.

  • 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.

  • 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.

  • 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.

  • 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.

  • 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.

  • 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.

  • 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.

    Update: They later modified the compiler to generate an error if the
    append() function’s result is not assigned to anything.

  • 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).

    Read More

    Share This Article
    Leave a Comment

    Leave a Reply