Live data from Hacker News

Why Go Is Not Good

yager.io

241–250 of 367 posts

Re: Why Go Is Not Good

#241

> A Good Solution: Constraint Based Generics and Parametric Polymorphism > A Good Solution: Operators are Functions > A Good Solution: Algebraic Types and Type-safe Failure > A Good Solution: Pattern Matching and Compound Expressions People have tried this approach. See languages like C++ and Scala, with hundreds of features and language specification that run into the thousands of pages. For an unintentional parody…

The Scala specification is two hundred and something pages, around a third the length of the Java specification (largely because Scala has, in some sense, fewer features than Java, in the sense that Java has lots of edge cases with their own special handling, whereas Scala has a smaller number of general-purpose features. The complexity comes because it's easy to use all of them at once)

Re: Why Go Is Not Good

#242
post #49

Earlier quoted context omitted.

>In modern systems, simplicity is a powerful debugger. I don't consider the features I mentioned in this article to constitute "complexity". I think that Haskell is a beautifully simple language, in the same way that e.g. Euler's identity is beautifully simple. The reasoning behind it may be somewhat complicated, but the result is very simple (and impressive) to behold.

So why not just use Haskell? Why didn't Haskell take over Go's niche?

I do (well, Scala), and it's much nicer. So it's frustrating to see all these articles praising this Go and encouraging beginners to learn it, or claiming it's innovative when it's nothing of the sort.

Re: Why Go Is Not Good

#243
post #231

Earlier quoted context omitted.

> Rust...with its arrows, angle brackets, pattern matching, etc. seemed just so complex to fit in my brain. It's 2014 already. Angle brackets, arrows, and pattern matching are 1990 level language technology. Heck, even a dynamic front-end language like Coffescript has these kind of things nowadays.

I'm not saying you're wrong nor will I argue about it as in the end it really comes down to taste I feel. To me, and perhaps to others, these things make a language harder to read/use, regardless of how pervasive...

While some of it can come to taste, I think having higher abstractions (as long as they don't leak) is beneficial to reading/using a language. Like, objectively.

And foreach vs for is not a leaky abstraction. Nor is pattern matching vs traditional checking and extracting the values. So their value is not geting lost on having corner cases that the equivalent "based on primitives" code wouldn't have. If anything they make the operation even more explicit.

So I think a lot of it comes down to getting familiar with them.

While something made of "first principles" might be more instantly familiar, it will be harder to reason about because of its low-level ness as the code size increases.

Case in point, assembly. It's quite primitive language with very few constructs, so it's easy to learn to use. But an actual high level "if" or function call is much easier to grasp at once than checking 10-20 lines that implement the same thing in assembly (even if you have trained yourself to see the assembly "pattern" for a function call, etc).

Re: Why Go Is Not Good

#244
post #233

Earlier quoted context omitted.

> It explains how natural language is a very poor way to express programs, and how it held back science and progress for many centuries. I'm not interested in using natural language to implement the software (write code). I'm interested in using natural/technical language to create an ontology for the architecture. This is where things get gray. When I think of an architecture, I think of something that evolves over…

> This ontology evolves over time as the software, understanding of the domain, & the domain itself changes. So what does this have to do with Go or type systems? > I usually use guard clauses to protect against nulls. I rely on my tests & production monitoring systems to prove that the implementation is incorrect. And that's a bad thing. There are better tools for this job. What's the downside of encoding nullabilit…

> This ontology evolves over time as the software, understanding of the domain, & the domain itself changes. > So what does this have to do with Go or type systems?

I've found that type systems, that aren't utilizing Duck Typing, as being restrictive & causing incidental complexity when evolving the design. I don't really care if something is a categorization of something else. I usually (> 98% of the time) only care if that something adheres to an interface.

I don't like to label people in life either :-)

> I usually use guard clauses to protect against nulls. I rely on my tests & production monitoring systems to prove that the implementation is incorrect. >And that's a bad thing. There are better tools for this job. What's the downside of encoding nullability into the types?

For the web, it's not that bad. The design is constantly evolving, so most of the development period is spent completing something that is not finished.

There's no downside in encoding nullability, unless extra syntax & incidental complexity is added. It's not a big problem for me so I'd rather not have to do extra work for this feature.

> That mostly sounds good. I would want invariants to be optional, which sounds like is the case. > Every good type system lets you "opt out".

Explicitly or implicitly? I'd rather be opted out by default and opt in when I want to. Again, I don't want to do extra work or have incidental complexity.

Re: Why Go Is Not Good

#245
post #21
post #7

Every single thing listed in the article can be added to Go at any point, since it currently has a very minimal feature set. Once something like generics are added, they have to support it until the end of time or risk having an unstable API like Rust did for a while there.

> Once something like generics are added The notation in id (item for generic bracketing is harder to read than other bracketing symbols, e.g. () [] and {}. Unlike those others, angles are used for comparison ops and arrow heads also. If Go ever introduces generics, the Scala-like [] notation looks cleaner and would fit into Go's existing grammar.

The only point of confusion I can see out of using [] is:

    type Foo[T] T
versus

    type Foo []int
for example.

    type Array[T] []T

Re: Why Go Is Not Good

#246

Earlier quoted context omitted.

Lack of generics is part of the reason why Go is easier to learn and the Go compiler is faster than, say, Rust. Sure, generics don't have runtime overhead in Rust and Haskell but they have other costs. You always pay for abstractions some way.

Last I checked compilation time wasn't really a thing a ton of folks worry about (myself included). Faster machines and reasonably better compilers have mostly solved this problem. I'd take zero-overhead generics over a slightly faster compiler any day of the week .

Speak for yourself - slow compilation drives me crazy, as it increases the latency between making a change and seeing the result. Not that I believe that generics require slow compilation.

Re: Why Go Is Not Good

#247
post #237

Earlier quoted context omitted.

Isn't the C equivalent of a slice just a struct containing a *T, a length and a capacity?

And some methods for manipulating it (slicing it), and reference counting. And macro's for automatically ref'ing/unref'ing.

Reference counting? I didn't know reference counting was used with slices, I thought they just used the GC.

Re: Why Go Is Not Good

#248
post #50

There's something very seductive about languages like Rust or Scala or Haskell or even C++. These languages whisper in our ears "you are brilliant and here's a blank canvas where you can design the most perfect abstraction the world has ever seen." But, for systems programming, abstractions suck. They always, always have a cost. When abstractions break, you not only have to deal with a broken system but the broken ab…

I'm not writing the following to make people pick another language - if Go is suitable for your project in terms of features, runtime, tools and community, then by all means use Go. It's a fairly decent platform to target and it can further evolve to meet more stringent needs.

But if we are talking about the cost of abstractions, the biggest elephant in the room is that Go's GC is NOT optional, which makes it unsuitable for ... (1) systems programming and (2) real-time systems.

C++ and Rust do not suffer from this. And Go is not even suitable for soft real-time systems, because for that you need a GC that never stops the world - right now Go is even less suitable than Java in this regard, because at least for Java you've got the pauseless GC from Azul Systems.

> "But, for systems programming, abstractions suck. They always, always have a cost."

That's a logical fallacy, because if all abstractions suck, then why aren't we doing "systems programming" in assembly (were systems programming is whatever the definition du-jour you prefer to fit Go in)? Clearly, it depends on the project on where it can draw the line, since we are always doing compromises for gained productivity, no? And going back to the non-optional garbage collection that's not even suitable for soft real-time systems, it kind of makes the point on Go avoiding higher-level abstractions on purpose kind of bullshit.

> "It does not make it possible to make things pretty (ugh, nil)."

It's not about pretty-ness, it's about correctness - which in a language containing memory unsafe constructs that can lead to billion dollar bugs (i.e. Heartbleed), is a freaking huge deal. Rust is very innovative in this regard, because it's a systems programming language that solves many issues by means of its more advanced type system - and surely no type system is perfect, but even a single bug that's caught by the compiler, that's a bug that won't reach production.

> "It just makes it impossible (ok, really hard) to overcomplicate things."

I wish developers would stop equating "complicated" to things "I don't understand". That's not what complicated means. Here's the definition: "consisting of many interconnecting parts or elements". That Go doesn't allow certain higher-level abstractions, that's in itself a recipe for complications.

> "for tools that only need to do one thing and do it extremely well, it's either that or C. And I'm not going back to managing my own memory anytime soon"

The choice between C and Go, given that Go is garbage collected, is a false dichotomy.

Re: Why Go Is Not Good

#249
post #81

Earlier quoted context omitted.

I don't agree with the GP, but if Go were very similar to C and the only big difference would be that C has no GC it would pretty easy to picture what the C equivalent of Go code would be. Exactly the same but with calls to `free()` at the end of some functions. (or preempted between instructions at unpredictable places) Don't make GC's a bigger deal then they are. They are a tool to remove the need to call `free()`…

There's also the overhead of the mark phase, which has to work out dynamically what could be worked out statically in a system with manual memory management. That's where much of the overhead of GC comes from.

I'll add to that that the overhead is not proportional to the amount of garbage your application generates, it's proportional to the amount of data blocks your application has allocated. Garbage collection is also often performed by suspending the application (stop the world) at unpredictable moments and with an unpredictable duration. Garbage collection can thus be a serious problem for some type of applications. This is why the GC should be optional.

Re: Why Go Is Not Good

#250

For fear of disagree downvotes: I would say that many of the qualms brought up in this article are problems that are encountered fighting the language. The problem of 'summing any kind of list' is not a problem that is solved in Go via the proposed kind of parametric polymorphism. Instead, one might define a type, `type Adder Interface{Add(Adder)Adder}`, and then a function to add anything you want is fairly trivial,…

>When it comes to iteration, there is the generator pattern, in which a channel is returned, and then the thread 'drinks' the channel until it is dry, for example `func (m myType) Walk() chan->myType` can be iterated over via `range v := mt.Walk(){ [...] }`. Non-channel based patterns also exist, tokenisers usually have a Next() which can be used to step into the next token, etc.

Actually using channels as a general iterator just for the sake of using the range operator is considered as an anti-pattern. The reason is not performance (although it has a cost), but the risk of leaking producer goroutines. Your example:

    for v := range mt.Walk() {
      if blah {
        break
      }
    }
How will the goroutine writing into the channel returned by mt.Walk know when there are no more consumers which will possibly read from it?

One way out is:

    done := make(chan struct{})
    for v := range mt.Walk(done) {
      if blah {
        break
      }
    }
    close(done) // or defer close(done)
Picking the right cleanup is error-prone.

What about errors? How will mt.Walk tell you that it had to interrupt the iteration because an error happened? Either your channel has a struct field containing your error and your actual value (unfortunately Go lacks tuples or multivalue channels).

Furthermore uncaught panics in the producer goroutine will generate a deadlock, which will be caught by the runtime, but it will halt your process. One way to do it is:

    errChan := make(chan error)
    for v := range mt.Walk(errChan) {
      if blah {
        break
      }
    }
    err := 
The producer will use the select statement to write both to errChan and your result channel. The success of writing to errChan is a signal for the producer that the consumer exited. However same thing here about relying on the last statement being executed to avoid a leak in case of returns or panics. Here the defer is less nice since you're supposed to do something with the error:

    func Example() (err error) {
      errChan := make(chan error)
      for v := range mt.Walk(errChan) {
        if blah {
          break
        }
      }
      defer func() {
        err = 
Next-style methods just pass through the panics, and allow you to handle errors either by having a func Next() (error, value) or with this pattern which moves the pesky error handling outside:

    i := NewIterator()
    for i.Next() {
      item := i.Item()
      ...
    }
    err := i.Error()
First, any panic that happens inside either your code or the generator will bubble through. Second, if you return from your loop body, you will have to provide your own error (the compiler will remind you about your function signature, if in doubt). You can return early if the iterator can be stopped and GCed out (i.e. it doesn't handle goroutines or external resources), otherwise you'd have to call a cleanup as with channels.

The rule of thumb with Go should be that you don't have to do things just because they use some syntactic sugar. After a while you start to think about beauty in terms of properties not about calligraphy.

However, I do see this as a weak point of the language, which hopefully can be solved by education; after all Go is so simple to learn that you might be tempted to make it look even simpler. But the fact that the language has (almost) no magic, it means that you can actually understand what some code does, which imho outweighs the occasional syntactical heaviness or having to learn a few patterns.

Post reply on HN