Hacker News new | past | comments | ask | show | jobs | submit login
Avoiding Complexity with Go (bradgignac.com)
71 points by bradgignac on Oct 17, 2014 | hide | past | favorite | 85 comments



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.


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.


Thank you for taking my criticism well. Writing is hard and exhausting. I struggle with writing all of the time, even when I make goals to do it. I hope I haven't discouraged you from writing more.


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/ [2]http://docs.oracle.com/javase/7/docs/api/


Thanks for the feedback, jasode. Most of my piece was written from the perspective of Ruby, Python, and Node.js development, so your perspective on the state of affairs in Java is helpful. In this regard, Java sits in an interesting middle ground between Go and Ruby. Go completely relies on compile-time dependencies. Ruby completely relies on run-time dependencies.

Once piece of consistent feedback that I've gotten is that I didn't do a good enough job of clarifying that I was specifically dealing with the complexity of running applications. I'm pretty sure most people who have deployed Ruby or Python web applications would agree that deploying and running those applications is more complex that it needs to be. The examples of accidental complexity you gave are great examples of how it can manifest in code design.

I really liked your last point about the importance of use cases. I briefly alluded to that in the conclusion of my article, and I'm glad that you brought it up here. Use-case is extremely important.


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


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

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.


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.


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


>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? :)


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

I agree with all that Golang simplification except for one thing: I don't call the previous state of affairs "accidental complexity". It's certainly "more complex" with more moving parts but it doesn't feel "accidental" to me.

I maintain that the nature of complexity and location of it has changed. You yourself gave an example:

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

Complexity has now increased in deployment if there's a bug. If Tomcat has a security bug, you update it instead of all the servlets. If golang exe has a bug, you have to recompile all exes and redeploy.

It doesn't mean the Java situation is overall better than Golang, it's just different.


> ... I had to go ahead and publish what I had or it would have remained in my drafts forever. ...

It wouldn't be a loss if you have deleted it forever, you didn't need to keep it in your drafts.


> But if Go becomes the next mass adopted language for writing business applications, that will be a huge mistake for our industry.

I felt that when people say "use the right tools for the right job" and go ahead build web-app using Go, they're doing the talk the talk but not walk the walk.


Have you written a web application in Go? It's actually quite nice. And the low memory overhead and self-contained nature of Go binaries means you can easily put multiple applications on the same machine with nothing more than a reverse proxy.


> But what about the criticisms of the language that are always left unanswered?

I think the article's answer was: Go solves the real problems, not the academic/theoretical problems. But it's fair to ask, how does Go do that? Specifically?


People claimed that for every programming language in history that was a complete fuck-up, whereas languages with academic/theoretical underpinnings are still alive and relevant.

The true tragedy of our industry are not the technologies we use. The tragedy is that we do not learn from prior mistakes - I was in school and into programming in the nineties and in 20 years things changed, but not that much - people are still debating basically Basic vs Pascal vs C and sometimes it feels like freaking groundhog day, the movie.

(N.B. I'm not criticizing Go here)


Because unlike, say Java, Go programs crater under complexity. Various attributes of the language, such as the module system, the type system, the visibility model, etc. make it very (very) painful to do architectural space walks.


Can you explain how those language attributes make projects more difficult to navigate?


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.


I don't understand your problem with errors versus maps.... they're actually the exact same code pattern:

    val, ok := m["foo"]
    if !ok {
        // handle not found
    }
    // good path

and

    val, err := m("foo")
    if err != nil {
        // handle err
    }
    // good path


3 very good (if minor) points here - it'd be great if the go team looked at fixing things like this for a go 2. I've heard talk they don't have any ideas of what might be in a go 2, but a few grammar tweaks like these plus pkg dependencies would be a good place to start. There are definitely some rough edges to the language and some decisions which in retrospect they might not have made, but since it's frozen at go 1, there won't be any changes for the foreseeable future. In some ways that's comforting if you're building real apps on the language, so not at all a bad thing.

I'm not even sure if they need a := operator, is the additional complexity and wasted time worth the benefit (compile error on redeclared variables, most of the time)? It feels a chore having to type out := most of the time and, as you point out, confusing where there are several variables assigned at once.

Another area which is unnecessarily complex is allocation: http://golang.org/doc/effective_go.html#allocation_new

You might use:

    func () (t T) {}
    var t T
    t = T{}
    t := &T{}
    t := new(T)
    t := pkg.New()
    t := make([]T,5)
Though the pkg.New() option is really just an optional convention. It all makes sense eventually, but do we really need all of these options? I'd rather have something like:

    t = T{}
    t = &T{}
    t = [5]T
Still, these are pretty minor niggles, which can mostly be overcome with conventions, except perhaps the use of nil everywhere, which is a shame. There's a lot to like, and the emphasis on simplicity (at the cost of features) definitely is a feature of go, even if it does have some rough edges it is significantly simpler than most other languages both to learn and to use.


Rob Pike mentioned variable declaration when asked at GopherCon what he wished he hadn't done in Go

http://youtu.be/u-kkf76TDHE?t=16m10s


The solution to number 2 (if it really needs one) is simple to use `if !ok` instead to push the exceptional case to the right.


#3 has always bothered me. I feel like:

   someString := "hello"
   someString, err := ReturnStringAndError("world")
   if err != nil {
      fmt.Println("there's no way this could happen")
   } else {
      fmt.Println(someString)
   }
Should produce an error.


It's a matter of pragmatism. When you have a sequence of functions that all return (res, err) pairs, it's extremely helpful to be able to use := even though the err is not redefined.

Occasionally, I wonder whether := with several variables on the left hand side should have been to defined to redefine variables (shadowing the earlier definition), but obviously the Go people thought that such shadowing would be worse.


Yup, this is why you don't need err1 err2 err3 err4... (I've had to do similar things in C# before).

Also, := does shadow in a sub-scope. The difference between shadowing and simply assigning in the same scope is negligible (i.e. either way you can't get at the old value).

    err := foo()
    if err != nil {
        err := bar() // err shadowed here
    }
    f, err := baz() // err assigned here
What would be the difference between shadowing and assigning on that last line? You can't unshadow without leaving scope, at which point the value you were shadowing also leaves scope.


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


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.


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


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


> 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

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


> 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?

Whenever you have null in a language, for every function you call that returns a reference, you have to check the documentation to see whether or not it will return null. You cannot know from only the function interface whether or not you have to deal with an optional value.

Option types move this checking to the compiler. The function declaration explicitly shows the presence of an optional value. You don't have to read the documentation or investigate the function implementation - the information exists in the function interface itself.

Given, Go tries to deal with this by using the multiple return idiom. By convention, you should only have to deal with null if you have a function with multiple returns. However, I think that compilers, not conventions, should enforce language rules. Leaving it up to conventions opens the window for human error.

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

This comes from Effective Go: https://golang.org/doc/effective_go.html#maps

They explicitly call it the “comma ok” idiom.


> This comes from Effective Go: https://golang.org/doc/effective_go.html#maps

> They explicitly call it the “comma ok” idiom

The "comma ok" idiom is the idiom of accessing maps w/ `thing, ok = m["key"]`, not the idiom of what to do with `ok` afterwards. Nothing there says to prefer `if ok {` over `if !ok {`.

---

I guess I can see a case for null-ability being more complex than not. I think the argument for them being simpler is that most of the time values are "actually" nullable (ie, the function can return nil and you should check for an error), in which case the implicit "pointers can be null" rule seems like a simpler mechanism than introducing option types (and, potentially, variant types or whatever other machinery will be needed to implement them).

As you said, the "return an err as well if the function can return nil" rule does a decent job of alleviating the complexity here. Compiler-enforced checks here seem like something that adds complexity (there's now more to the language), even if they do achieve stronger safety.


2. Objective-C uses the `if ok` pattern a lot because you can send messages to nil. Not that it's a great pattern, but it is quite common there.


Sorry, I meant specifically wrt. to Go.


I don't see #1 as a problem. Only pointers can be null, values cannot. Strings are not null terminated so that removes a whole class of problems.


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

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.


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.


> Much like ORMs are a trap

If that is the case, the same can be said with JavaScript, Rails, Ruby yes? (all of them looked simple yet you can screw up really bad, like awfully bad, like worse than Java complexity bad).

I use ORM to do simple-to-medium complex queries enough to avoid N+1.

My ORM also have tools around it to help me generate DDL from code as part of my build (of course one still have to ensure the generated DDL is correct with proper relationship and constraints and all that jazz, but my point stands).

My ORM gives me the ability to write in either JPQL and SQL to do certain tasks like deleting a bunch of rows based on conditions. Those are handy enough.

My ORM also helps me prevent against SQL injection attack too.

How are these abilities are "traps" for me just as much as the C++ complexity are traps?


I'd rather deploy go than clojure, but I don't know if the go authors achieved what Socrates and Rich Hickey had in mind.


I agree with you. There is a difference between simple and simplistic. Easy is not always simple.


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.


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


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.


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


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


> 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


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


But do those library functions actually exist or did you write them just now?


I wrote SortBy, but it was trivial, and you'd only have to do it once.

(The bigger issue is probably in Go you'd rather just have the boiler-plate than using []interface{}. But that's what you're essentially doing in a dynamic language like Python, and you can do it in Go, too, if you want.)


> and no worse than pre-generics java

Personally, that was the turning point when Java became palatable. Which is a shame, because Go is all-around a pretty nice language. But writing real, production code, I've felt that I was needlessly repeating myself because of the lack of generics. Specifically, I really wish I could write generics over channels, so I can solve the problems of multiplexing/demultiplexing channels, throttling them, etc exactly once.


(It's worth pointing out that Go does offer lots that pre-generics java doesn't, in particular interfaces w/ structural typing.)

Agreed that not having generics is a real pain-point, though not as bad as I would've assumed before using the language.

For the channel stuff, you could always just make use "chan interface{}", have your generic chan library, and then cast when receiving from the channel.

Or you could use code generation to make specific versions of your library for various types, maybe using the upcoming go generate.

It's kind of interesting, because C++ templates, for example, essentially are code-generation, but just hidden away, and for that reason tend to get overused (IMO), resulting in crazy-long compile times.

If Go's solution to generics ends up being "generate the code explicitly", it would seem uniquely Go-ish -- inelegant, but might work well practice.

(I do sort of dream about some kind of generics in Go -- something like

  where X interface{}
  type LinkedList X struct {
    Obj X
    Next *LinkedList X
  }
and maybe...

  where X Lessable
  type Heap X []X

  func (h *Heap X) Push(x X) {
     ...
  }
or even just

   where X interface{}
   func PushHeap(h []X, x X, less func (a,b X) bool) {
      ...
   }
Ah, to dream :)


> no worse than pre-generics java

exactly.


> obj-c

Objective-C is essentially dynamically typed.


Truth, but it does let you write down types for method arguments and instance variables, and the compiler will warn(!) you if you violate those -- if you're sane, you'll treat those warnings as errors.

So it's got a static typing feel, even if it is all just dynamic method lookup at runtime.


>but no worse than every single line written in a "dynamic" language like Python

Yes, much worse.

>and no worse than pre-generics java

That is setting the bar so low it is underground. I don't want "no worse than a language that was born obsolete 20 years ago", I want "good".


I write Go all day every day and have since before Go 1.0, and I see this troll/complaint at least 100x on hacker news for each time I have to do it once in real life.

On the rare event this actually matters I use templating tools if performance is needed, but if not (which is usually the case) I use an interface. But like I said, this is so rare.

P.S. I write distributed databases / big data engines and distributed content optimizing proxy servers.


> I write Go all day every day and have since before Go 1.0, and I see this troll/complaint at least 100x on hacker news for each time I have to do it once in real life

I suspect that there is as much trolling in people who complain about generics, as there is survivor bias in those who reply they haven't needed them in X years of go. If you felt that you needed them, you wouldn't have lasted long using go!


Those are fair points. I also think major part of the issue here is all the folks that try to write Java/C++/Python,etc in Go, rather than Go in Go. People install Go, write something in Go, paying no mind to go idioms, and complain. It happens on the Go mailing list all the time.


I truly don't see many people talking about leaving go after getting frustrated with no generics. If this were survivor bias, where are the casualties?


I assume most people who complain about Go's lack of generics have at least tried the language. It doesn't take long to run into your first interface{} and subsequent typecast. So, if you define "leaving Go" as "not switching to it", there are many. :)


It seems like quite a leap of faith to assume a significant portion of people complaining about something on the internet have actually tried it. Most complaints about Go tend to show the opposite. For example "it doesn't take long to run into your first interface{}"... what? Sure it does. If you're attempting to write Go like Go (and not trying to write Go like Java/C#/Haskell/whatever) then you can code for a very long time without hitting your first interface{}.


Don't get me wrong, I'm not saying that interface{} is a terrible thing. I spent a decade writing assembly language and then just as much in C and C++ before templates were usable, I know generics are far from essential and that a little controlled type unsafety is not a huge problem in practice. But:

> you can code for a very long time without hitting your first interface{}

Negroni, a popular and self-appointedly idiomatic library: https://github.com/codegangsta/negroni/blob/master/negroni_t...

    func expect(t *testing.T, a interface{}, b interface{})
    func refute(t *testing.T, a interface{}, b interface{})
Ok, that's just for testing - it's not unusual to have to go off path to be able to test your code. How about Google's own standardized idiom for production code? https://blog.golang.org/context

    Value(key interface{}) interface{}
Or Go's own standard library? http://golang.org/pkg/encoding/json/

    func Marshal(v interface{}) ([]byte, error)
    func MarshalIndent(v interface{}, prefix, indent string) ([]byte, error)
    func Unmarshal(data []byte, v interface{}) error
No, it does not take such a long time.


The context example is the only one of these that is really a valid example - and that is not something you would normally run into. I've never seen that idiom in code outside of google, so I wouldn't even call it idiomatic, regardless of the fact that it's posted on the Go blog. I'm actually sort of surprised they even posted it there.

The JSON package is handling automatic marshalling of objects to and from a textual format - of course it has to be outside the scope of normal code.

When I said "you can code for a very long time without hitting your first interface{}", I meant in code you write. It's very rare to need interface{} in your own code. Some very specialized packages (like JSON) need it, and yes, sometimes helper functions for testing need it. But for regular production code? No. If you find yourself reaching for interface{} more than extremely rarely in your code, you're probably doing something wrong.


From the article:

> Go isn’t perfect for every task. [...] Ultimately, it is up to you and your team to decide the best language and tools with which to build your application. While making that decision, I hope that you will take a moment to weigh the trade-offs that come with your choices.

It's a tradeoff based on your real, hard needs. If you decide that only having Array<T> and Map<T> is a deal breaker, and the complexity of creating a tool to autogenerate the go code for a large number of SinglyLinkedList<T>/DoublyLinkedList<T>/CircularLinkedList<T>/etc... outweighs the uses of Go, then don't use Go.

If you are fine writing a quick dirty code generator that can look for some <T>-like syntax and do simple replacement, or only really need Array<T> and Map<T>, then consider Go.

It's not like the language is forcing you to use it against your will.


> If you decide that only having Array<T> and Map<T> is a deal breaker, and the complexity of creating a tool to autogenerate the go code for a large number of SinglyLinkedList<T>/DoublyLinkedList<T>/CircularLinkedList<T>/etc... outweighs the uses of Go, then don't use Go.

With all due respect, but how is having to build a custom code-generator first, before you can write your actual program not accidental complexity?


I wasn't claiming it isn't accidental complexity. Just pointing out the author essentially says to weigh the decision appropriately.

Looking at the sibling comment about "go generate" by buro9, it looks like a custom code-generator won't be needed either.

EDIT: Added buro9, and emphasis


Not yet, anyway.


go generate is in 1.4 which will arrive shortly

The proposal (the implementation is pretty much this) is here: https://docs.google.com/a/golang.org/document/d/1V03LUfjSADD...

If you feel a real strong need for generics, for something like a "sort by property of my custom type... do it for n types", then it's now trivial to generate that.

You can code like you have the features the core doesn't support, whilst having the succinctness, performance and maintainability that the language delivers.

I've coded Go almost daily for 2+ years and only a couple of times had the urge/need for generics... I dunno, maybe I'm the exception here.


I've had the same experience - very little need for generics in Go on a regular basis. I really appreciated the following snippet from http://robnapier.net/go-is-a-shop-built-jig:

"Probably the biggest complaint people have with Go is the lack of generics. And I did run into that in just the first couple of weeks of work on my project, and I wound up with a bunch of duplicated code to work around it. And then, when it was all working, I refactored out the duplicated code. And I refactored again. And in the end, the whole thing was simpler and shorter than what I would have done with generics. So again, in the end, Go turned out to be a language for solving real problems rather than a language filled with beautiful tools, and so you build real solutions rather than finding excuses to use your beautiful tools. Don’t try to make Go what it isn’t. If you’re trying to solve abstract CS problems in their most generalized forms, then you may find it frustrating. But if you’re trying to solve specific, practical problems in the forms professional developers typically encounter, Go is quite nice."


So ..... he ended up doing far more work than he would have done had Go been like any other modern language? Without detail about what these refactorings were and why he didn't write the code that way at first, what lessons he drew from it etc this anecdote is not very useful.


> eg bindata: translating binary files such as JPEGs into byte arrays in Go source

I'm doing that myself in a terribly hacky way using custom shell scripts and a bit of python to print out a .go file -- but seeing it in this list makes me wonder if it is a common problem that has been solved better already - does anyone know of any tools for this? (I imagine something like "bin2src -i foo.jpg -o foo.go", with support for a variety of languages)


I guess real-world softwares don't need fancy containers other than a list and a map, and when they actually really do need them the copy-paste turns out to not be that much of a burden ?


People need to start downvoting the "Go doesn't have generics" trolls. This has been addressed many times. If it really is a showstopper for you, that's fine. There's no value in trolling Go submissions.


Is there some rule that only people who are done talking about generics may participate in conversations about Go? I was happy to see this discussion here.


It's your "Ground Hog Day". I'm not sure how much more there is to get out of the comment: "Go doesn't have generics, doesn't work for me."


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.


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.


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.


I have been playing around with go and found this issue. http://stackoverflow.com/questions/26411121/go-calling-c-fun...

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.


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.


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


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



This article is about an opinion on writing software well, not writing good software.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: