
Avoiding Complexity with Go - bradgignac
http://bradgignac.com/2014/09/24/avoiding-complexity-with-go.html
======
rubiquity
It's hard to believe that this article would be on the front page if it didn't
contain a reference to the Go programming language in the title. There is no
technical meat about how to avoid complexity with Go in this article. It's a
rehash of all the same things that have been circulating around the internet
about Go recently.

Yes, the standard library is great. Yes, it's nice your programs compile to
binaries. But what about the criticisms of the language that are always left
unanswered?

I like Go a lot as a language for writing networking software. But if Go
becomes the next mass adopted language for writing business applications, that
will be a huge mistake for our industry.

~~~
bradgignac
Fair comment -- thanks for the feedback. I've received similar feedback from a
friend who reviewed the post, and I completely agree. To be honest, I had to
go ahead and publish what I had or it would have remained in my drafts
forever. However, I'd like to follow up in the future and address some of the
points you raise.

~~~
jasode
If you're taking feedback, I'd like to disagree on your examples of
"accidental complexity".

To me, accidental complexities are quirks of design that everyone would agree
to do over differently if history replayed itself. Examples like that would
be:

\-- Java calendar (and C Language ctime) months have index starting with 0
which means 0..11 for Jan..Dec but for days, it's index 1 which means 1..31

\-- PHP inconsistencies such some functions being verb_obj while others are
obj_verb and some have underscores and some do not

\-- MS Excel has incorrect leap year calculation for 1900 which means all
other spreadsheets must duplicate this "bug" to be compatible

Those are the types of "accidents" that arise out of ignorance or
uncoordinated thinking. Andrei Alexandrescu (expert in C++ and D) has a
similar concept he calls "unforced errors".

On the other hand, you compared a Tomcat servlet container as being
"accidental complexity" and Go as "reduced complexity" because the http
hosting is all built into the exe. To me this is an example of _moving
complexity around_ so you don't feel it _for your particular use cases of the
Go language_.

In other words, if the Java community had a chance to do it again, I'm not
convinced that Tomcat servlet would be baked into the standard Java
distribution. If you bake it into the JVM, you've made the JVM
Specification[1] more complex. If you leave it out of the JVM but include it
as a standard class library, you've made the API library reference more
complex[2] (Alternative universe: Why is the class library reference 3000
pages long?! Because it includes 200 pages to document a servlet container
that most don't use.)

Complexity (in totality) wasn't reduced. It was only shifted around. I'm not
saying moving complexity around isn't desirable because it is (hence we have
"abstractions") but it's not an example of solving accidental complexity.

I think what happens is that we programmers find some language, or framework
and if it matches our use cases, it's _subjectively_ less complex. For the
others who need generics to write algorithms, they write homegrown template
code generators or resort to copy&paste which is _more complex_. So sure,
Golang omits generics but it only makes it _less complex_ from a language-
specification perspective. However, it's actually _more complex_ from a total
project perspective. I think this explains all the contradictory posts about
Django/Rails/etc being "simple" while other posts say it's "complex pulling
teeth".

At this point, I don't buy that golang without qualifiers of use-cases is
_objectively_ less complex. I'm writing server-side web services with golang
and for that use case, I think it's less complex than other alternatives such
as C++ or Nodejs. For Windows GUI apps, using C++ with Qt is less complex than
Golang.

[1][http://docs.oracle.com/javase/specs/jvms/se7/html/](http://docs.oracle.com/javase/specs/jvms/se7/html/)
[2][http://docs.oracle.com/javase/7/docs/api/](http://docs.oracle.com/javase/7/docs/api/)

~~~
AnimalMuppet
I think you missed the author's point here. You have to have the webserver
somewhere. That's inescapable complexity. It's in the language, or in the
library, or in an external container like Tomcat. But if you're going to run a
web app, you have to have a server somewhere.

Where Go made things simpler is in the dependencies. The app now has all the
dependencies compiled in, and so it's completely isolated from what libraries
some other app in the container needed. _That_ is a removal of accidental
complexity. (Right up until you need to update all the apps to fix a bug in
some library, and now you have to update each app, not just update one library
in the container...)

~~~
bad_user
I don't get the comparison with the JVM.

I've built web services with an embedded Jetty in which the deployment
happened by copying and executing a single JAR file with everything included.
It's actually really easy to do -
[http://www.eclipse.org/jetty/documentation/current/embedding...](http://www.eclipse.org/jetty/documentation/current/embedding-
jetty.html)

Now I'm building web services on top of Play Framework (and Scala), which
comes with its own web server that has nothing to do with Servlets or Java EE
and so it has nothing to do with application servers and WARs and containers.
And it's been good for the Play framework to come with its own server, because
Servlets is moving slowly (that's what happens to standards) and Play has been
capable of WebSocket or asynchronous processing of requests long before
Servlets was.

This is actually an area for which I love the JVM - Java's Servlets is an
objectively good standard with multiple implementations, other platforms can
only dream about such a popular and good standard (Java EE on the whole
arguably less so, but the Servlets part is OK) and you can totally decide to
not use it. I personally love having choice and when I'm faced with a problem,
it's much easier for me to search and learn a new library than a new language
- I'm saying this after I've been working with about 6 languages on the job
and played with about a dozen others.

Also, here's another opinion - I personally started to hate languages coming
with "batteries included". I like languages in which the standard library
contains the core necessities. Like, immutable data-structures or abstractions
for dealing with concurrency - completely fine to be in a standard library.
JSON parsing, http handling - no freaking way.

~~~
NateDad
Can you explain why you don't like languages with batteries included? Isn't it
nice to have one JSON parser that you know is quality and that everyone can
use? People new to the language don't have to go figure out which of the 8
libraries that are out there is the "good one". Standards like that also mean
that you can jump into any random codebase and know exactly what's going on.
So.... can you explain? I don't understand why they're a bad thing.

~~~
bad_user
> _Isn 't it nice to have one JSON parser that you know is quality and that
> everyone can use?_

Sure it is - but that's an utopia and never, ever happens.

The JSON parser in Scala's standard library sucks. The JSON parser in Python's
standard library sucks. The JSON parser in Ruby's standard library sucks. I
actually challenge you to give me an example of JSON functionality included in
a standard library that doesn't suck.

In practice what happens is that one or two third-party libraries pop-up at
some point that are so much better that people start using it as a de-facto
standard and then the functionality in the standard library becomes legacy
that has to be carried around because backwards compatibility.

And much worse than a small standard library is a standard library full of
deprecated stuff.

~~~
NateDad
>I actually challenge you to give me an example of JSON functionality included
in a standard library that doesn't suck

Uh... the one in the Go standard library? :)

------
jephir
Go still has some unecessary sources of complexity, for example:

1\. It uses null (the billion-dollar mistake) to indicate optional values. A
better designed language uses an option type.

2\. It has conflicting idioms for conditions. Error types use the `if err !=
nil` idiom while map access uses the `if ok` idiom. This means that you read
the main execution path downwards and the error path to the right, unless you
access a map, then the execution path goes to the right and the error path
goes downwards.

3\. The `:=` operator declares new variables, unless you use it to declare
multiple variables where one already exists in the same scope. As a result,
you don't know if you have actually declared a new variable using `:=` unless
you read upwards to see if a declaration for the same variable name already
exists.

~~~
alecbenzer
1\. Mistake or not, allowing all pointer types to be `nil` seems like
simplification from things like option types. How do you see nil as adding
complexity?

2\. I've never heard of `if ok` being an idiom _as opposed_ to `if !ok`.

~~~
tikhonj
Nil adds complexity because it creates a hidden failure condition _in every
possible value_. One that's not necessary most of the time.

Option types, on the other hand, _are not_ a source of complexity, because
they just use a more general language feature (ie variants). And variants
aren't a source of _significant_ complexity because they're symmetric to
records (ie structs) and so emerge naturally. Symmetry inherently simplifies
design by organizing and structuring things. They're a very small step beyond
enums and a much saner alternative to unions.

Nils are ultimately more complex because they're baked into the language and
omnipresent. And they give you very little in return! Variants, on the other
hand, are a natural and relatively simple design that also vastly increases
the expressive power of the language. And gives you option types for free.

~~~
burntsushi
> Nil adds complexity because it creates a hidden failure condition in every
> possible value.

That's simply not true in Go. `nil` is not an inhabitant of all types.
Integers, floats, strings, arrays and structs cannot be `nil`. Slices, maps,
functions, channels, interfaces and pointers are nilable.

> Option types, on the other hand, are not a source of complexity, because
> they just use a more general language feature (ie variants).

Go does not have variant types, so if adding option types requires them, you
get an increase in the size of the language. In Go's case, variant types would
have a weird interaction with interfaces, which arguably increases complexity.

~~~
waps
> That's simply not true in Go. `nil` is not an inhabitant of all types.
> Integers, floats, strings, arrays and structs cannot be `nil`. Slices, maps,
> functions, channels, interfaces and pointers are nilable.

True. But beside the point. Yes some things can be nil and some things can't.
Problem is that there is nothing stopping me from doing this :

    
    
      func (x *X) {
        x.boem()
      }
    

and that can crash. With ADTs ("option types (assuming a Haskell-like Maybe
type) the compiler would complain : "x can be of type Nothing, so you can't
just call a method on it".

The point is that Option types mean that you can still have optional values,
but you can never have nil pointers.

> Go does not have variant types, so if adding option types requires them, you
> get an increase in the size of the language. In Go's case, variant types
> would have a weird interaction with interfaces, which arguably increases
> complexity.

They would have exactly the same interaction with interfaces as they would
have with anything else. ADTs are not of any definite type, so you have to
case select them in most cases, and you can forego nil checking for everything
else.

You would have the "grouping" behavior anyway, since in Go you don't declare
that you satisfy interfaces. So if all possibilities for an ADT implement the
same interface, then the ADT should magically implement it too. I'm sure it'd
be a change in the compiler, but it wouldn't be a change in the language.

~~~
burntsushi
> True. But beside the point.

Uh. No. There is a big difference between "every value can be nil" and "only
some values can be nil."

And you don't need to sell me on the benefits of ADTs. All else being equal,
I'd much rather have them. But this doesn't mean I want to go around shoving
them into every language under the sun. I recognize that, sometimes, it's
reasonable to persist without them. I very strongly believe that there is no
One Right Language Design.

In Go's case, I've written a lot of it, and experience tells me that `nil`
errors just aren't a large source of bugs like they are in a language like C.
I suspect it is partially because you can dispatch on `nil` values[1], and
also partially because of _very very_ strong idioms like `if err != nil { ...
}`. You can also `append` to `nil` slices.[2] I recognize that this is
_practical experience_ and that it will always lose against theoretical
purity, but Go isn't after theoretical purity. (Please be careful. This is not
a claim that the two things are mutually exclusive.)

> You would have the "grouping" behavior anyway, since in Go you don't declare
> that you satisfy interfaces. So if all possibilities for an ADT implement
> the same interface, then the ADT should magically implement it too.

You're not thinking through everything. What happens when the discriminants of
a sum type are themselves interfaces? What happens when you type assert? Which
value do you get?

What is the zero value of a sum type?

How are sum types deconstructed? (Pattern matching! But now you've added
another language feature!)

Also, interfaces already provide some of the use cases of ADTs with type
switching. You just don't get compile time safety. So now you have a case of
non-orthogonal features.

Finally, you should note that I am _not_ claiming "these things cannot be
resolved." I am claiming that, "it is hard to resolve these things in
obviously simple ways that are consistent with the rest of the language." By
the time you're done resolving them, you will have made the language
specification more complex.

[1] -
[http://play.golang.org/p/4_SNEi9YgR](http://play.golang.org/p/4_SNEi9YgR)

[2] -
[http://play.golang.org/p/D3WRreGNBb](http://play.golang.org/p/D3WRreGNBb)

------
llambda
Whenever the topic of "simplicity" in software comes up, I feel obligated to
point to the superb "Simple Made Easy" talk:

[http://www.infoq.com/presentations/Simple-Made-
Easy](http://www.infoq.com/presentations/Simple-Made-Easy)

From my personal perspective, I do not see Go achieving the kind of simplicity
Rich talks so eloquently about. Instead, Go seems much more like an "easy"
language.

An example of easy versus simple in the OP's article is pointing to on
boarding: Sure, your on boarding of new engineers may be /easier/ because Go
is an ostensibly "simple" (they actually mean small) and familiar syntax. But
that does not imply any correlation with writing simple software. I would
argue the difficulty of writing abstractions in Go (especially around
channels) actually tends to yield the opposite!

Much like ORMs are a trap because they seem simple, so too are technologies
which have such a specious quality of simplicity. It is important to establish
how a given technology actually achieves simplicity in practice and I do not
see how this article argues that successfully--that is not to say Go cannot
achieve simplicity, but merely that this article does not seem to make a solid
case, in my opinion.

~~~
NateDad
I think most people assume that when people say Go is simple, they mean easy.
I think it's exactly the opposite. Go _is_ simple, but it's not always easy.
It's like the difference between building a house using pre-fab walls, and
building a house using studs and nails. Which one is easier? Probably pre-fab
walls. Which one is simpler? Probably studs & nails. You don't need a crane to
put the walls in place, you can do it with just a hammer and 1-2 guys. It
might take a little longer, but you'll have exactly the house you want.

Your simple/easy comparison with an ORM is a very valid one, I think. ORMs
seem easy, but they're not simple, and often times their easyness at the
outset causes complexity once you have to do anything that goes off the rails
they've laid out for you.

But I think Go is the opposite of an ORM. There's very little magic, nothing
gets done "for you". The code does what you tell it to do, no more, no less.
Which means people reading the code can immediately tell what it does - it
does what it says it does in plain terms.

------
serve_yay
We talk about complexity a lot, to the point where "simple" often really means
"I like this", and "complex" often means "there is something about this I do
not like, or do not understand".

About the reference to "Go is a shop-built jig", it seems to me one way of
reading that piece is that Go offloads the complexity to you to deal with,
rather than offloading it to the toolchain (all the Ruby Gems, rbenv, etc
stuff the author mentions). If you like to work that way then cool, but I
would question the conclusion that the overall complexity has really changed.

------
cageface
I guess having to reimplement basic algorithms for every new container type is
an example of avoiding complexity then?

~~~
dilap
Come now -- you don't have to re-implement containers for specific type in Go
any more than you do in, say, Python -- you just have to cast when you remove
objects from the container.

Not great, since you're losing compile-time safety for the type -- but no
worse than every single line written in a "dynamic" language like Python, and
no worse than pre-generics java, say, or obj-c.

~~~
m0th87
Python's dynamic typing saves you a lot over static typing _without_
parametric polymorphism, as in Go's case.

As a concrete example, take sorting. In Go, you have to implement
`sort.Interface` for each custom type you define. An example taken from the
`sort` package:

    
    
        package main
    
        import (
            "fmt"
            "sort"
        )
    
        type Person struct {
            Name string
            Age  int
        }
    
        func (p Person) String() string {
            return fmt.Sprintf("%s: %d", p.Name, p.Age)
        }
    
        // ByAge implements sort.Interface for []Person based on
        // the Age field.
        type ByAge []Person
    
        func (a ByAge) Len() int           { return len(a) }
        func (a ByAge) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
        func (a ByAge) Less(i, j int) bool { return a[i].Age < a[j].Age }
    
        func main() {
            people := []Person{
                {"Bob", 31},
                {"John", 42},
                {"Michael", 17},
                {"Jenny", 26},
            }
    
            fmt.Println(people)
            sort.Sort(ByAge(people))
            fmt.Println(people)
    
        }
    

An equivalent in Python is quite a bit simpler:

    
    
        class Person(object):
            def __init__(self, name, age):
                self.name = name
                self.age = age
    
            def __repr__(self):
                return "%s: %s" % (self.name, self.age)
    
        people = [
            Person("Bob", 31),
            Person("John", 42),
            Person("Michael", 17),
            Person("Jenny", 26),
        ]
    
        print people
        people.sort(key=lambda person: person.name)
        print people
    

If it were just sorting, it wouldn't be that big of a deal. But there's a lot
of nice abstractions missing from Go due to the lack of generics - e.g.
higher-order functions like `map`.

~~~
dilap
Your examples aren't quite equivalent -- `people` in Go refers to a contiguous
chunk in memory of size len(people) x sizeof(People). Thus, in the Sort()
method, there needs to be something that binds the sort-algorithms need to
swap elements to the specific code to swap that many bytes. Go does this, at
the cost of some boilerplate, with the ByAge interface. (And other languages
have more elegant ways to do this.)

But we are doing something you can't do _at all_ in Python (barring
specialized array modules or similar).

The equivalent of the Python code would be something like:

    
    
        package main
    
        import (
            "fmt"
            "sort"
        )
    
        type Person struct {
            Name string
            Age  int
        }
    
        func (p Person) String() string {
            return fmt.Sprintf("%s: %d", p.Name, p.Age)
        }
    
        func main() {
            people := []interface{}{
                Person{"Bob", 31},
                Person{"John", 42},
                Person{"Michael", 17},
                Person{"Jenny", 26},
            }
    
            fmt.Println(people)
            SortBy(people, func(a, b interface{}) bool {
                return a.(Person).Age < b.(Person).Age
            })
            fmt.Println(people)
    
        }
    

where we have a library function

    
    
        // SortBy can sort anything -- just give a comparison function.
        func SortBy(elts []interface{}, less func(a, b interface{}) bool) {
            sort.Sort(byFunc{elts, less})
        }
    
        type byFunc struct {
            elts []interface{}
            less func(a, b interface{}) bool
        }
    
        func (a byFunc) Len() int           { return len(a.elts) }
        func (a byFunc) Swap(i, j int)      { a.elts[i], a.elts[j] = a.elts[j], a.elts[i] }
        func (a byFunc) Less(i, j int) bool { return a.less(a.elts[i], a.elts[j]) }
    

This is exactly the same, expect for instead of saying "Give me .age" like we
do in Python, we have to explicitly cast and say "I'm expecting a person, give
me .Age".

~~~
m0th87
> Your examples aren't quite equivalent -- `people` in Go refers to a
> contiguous chunk in memory of size len(people) x sizeof(People).

How it's laid out in memory is tangential to the original critique - namely,
that Go introduces complexity by not having parametric polymorphism.

> But we are doing something you can't do at all in Python (barring
> specialized array modules or similar).

Python not only allows this (I don't see why it wouldn't), it has built-in
support for an equivalent for sorting via rich comparison methods [1]. The
point is, though, that you don't have to.

As for the Go example, it's not equivalent because you're writing your own
boilerplate code. And it's throwing static typing out the window, which
doesn't seem very idiomatic.

1:
[https://wiki.python.org/moin/HowTo/Sorting#Odd_and_Ends](https://wiki.python.org/moin/HowTo/Sorting#Odd_and_Ends)

~~~
dilap
Hmm? Sorry, I wasn't trying to say Python doesn't let you define comparison
functions, I was just reiterating that it doesn't let you sort an array of
embedded objects, as opposed to pointers.

This is not a tangential difference. It's a real, important language decision,
with benefits and consequences.

If you restrict yourself to arrays of interface{}, like python, then you can
do exactly what python does, at the cost of losing compile-time type
checking...exactly like python :)

I believe my example _is_ equivalent to the python code; it doesn't contain
any boilerplate (which is something you have to write over and over again),
but rather a short library function, SortBy, which you would only have to
write once.

(The normal Go sorting routines _do_ contain boilerplate, since the Swap and
Len methods are basically copy-paste jobs.)

That said, I agree using interface{} everywhere is not idiomatic Go, and
certainly using things like map and fold in Go would be so awkward as not to
not be worth it.

But doing something like writing a heap class that required casting when
removing the object would be pretty reasonable, IMO.

You're not throwing static typing out the window, you're just losing it in
that isolated case. It's more like...pushing static typing slightly out of the
comfy spot on the couch. That's unfortunate!...but maybe not that bad -- after
all, dynamic languages get along alright w/o static checks anywhere. :)

------
herge
I can understand his comparison of networking libraries in go vs python, but a
lot of the benefit of go is that it's networking libraries were written within
the last decade. How will go manage complexity around adding new (and better)
networking code while maintaining backwards compatibility? Python fell into
the trap of urllib, urllib2, urllib3, urllib 42, etc.

~~~
bradgignac
IMO, this is one area where Go shines. In a non-compiled language, you have to
bear the burden of the upgrade process when writing code AND when running
code. For example, if you update Python on you application servers and there
is a breaking change, you break running applications that may not have been
touched for weeks, months, or even years. With Go, you build a binary at the
time the code was released and you never need to touch that binary again. Your
binary can run forever even if breaking changes are introduced later.

As with Python, you still need to update code in subsequent releases to work
with the breaking changes. However, in my experience, this isn't the hard
part. The hard part is keeping your existing code running while staying on a
recent version of your favorite language runtime. This is even more painful in
an environment where multiple application run on the same server. If you write
a new application using Ruby 1.9.x but an older application will only run on
Ruby 1.8.x, you need to either split them across multiple servers or update
the old application to run on 1.9.x. There are tools to handle this (rbenv,
rvm, chruby, etc), but this is exactly the type of complexity I talk about
reducing in my post.

------
jaswilder
I believe the author was really focusing on "operational" complexity more than
code complexity and language features.

For example, a language choice like using Python can add accidental complexity
because it usually requires a container (uwsgi, gunicorn, etc..), sometimes a
front-end reverse proxy (nginx, etc..), some way to package up your
application and dependencies (tarballs, virtualenv, etc.). You also have to
deal with deployment host issues like is Python2.6 required and available on
your OS, do you have multiple apps with conflicting shared libraries that need
to co-exist on the same host, do you need some system libraries installed on
the hosts, are components compatible w/ each other (nginx x.y.z + uwsgi a.b.c)
and can developers run that on Macs (dev laptop) and prod (linux).

I believe this is the accidental complexity the author is trying to highlight
that can be avoided with Go.

I think if you look at the overall system (dev workstations, deployment envs,
OS choices, CI, config management, ops, etc..), projects that use Go tend to
have fewer components to manage and integrate and result in a simpler overall
system.

------
tonyplee
I have been playing around with go and found this issue.
[http://stackoverflow.com/questions/26411121/go-calling-c-
fun...](http://stackoverflow.com/questions/26411121/go-calling-c-function-
order-of-import-fmt-import-c-is-causing-build-erro)

Folks from SO gave very good explanation on why.

But because of the problem, I somehow felt cgo is like a "hack" because of the
requirement that the C block code is in comments block and must be immediately
follow by import "C". An additional empty line before import will cause the
code build to fail.

What do you guys think?

p.s. Now I know requirement and I can live with it.

But for a full day, I have hard time figure out why my code build ok and next
moment after some "cleanup" it won't build anymore. Not until I finally id the
line space is real reason the build fail.

The error message also doesn't make any sense.

------
al2o3cr
There's an unstated tradeoff relevant to this piece: "accidental complexity"
versus verbosity. The Go community seems to have come firmly down on the
"typing more simple code is better", but that's not a choice that everyone's
going to agree with.

As a human-language equivalent, compare a jargon-laden sentence to the
equivalent paragraph in Basic English. The sentence requires prior
understanding of more things, but the paragraph may take a LOT of words to say
anything.

------
zeeshanm
Couldn't stop recalling this quote from pg while reading this article:

"Really good software is not software that is written according to some
particular design methodology. The really really good software is software
that does something fabulous. Although, if you’re trying to do something
fabulous it is unlikely you’d write the software in a lousy way, at least, and
succeed." \- pg

~~~
leetrout
Source? If this is a snippet from one of his essays I'd love to read the whole
thing.

~~~
zeeshanm
Fast fwd to 2:37:
[https://www.youtube.com/watch?v=BDA0t49AaZ4&feature=youtu.be...](https://www.youtube.com/watch?v=BDA0t49AaZ4&feature=youtu.be&t=2m37s)

