Hacker News new | more | comments | ask | show | jobs | submit login
Griping about Go (google.com.tedunangst.com)
63 points by dmit 15 days ago | hide | past | web | favorite | 86 comments



The whimsical nature of interfaces is definitely different coming from other languages. An `io.Flusher` might be nice, but it's not really necessary. You can just write:

    type Interface interface {
        io.Writer
        Flush() error
    }

    func someFunction(w Interface) error {
        w.Write(nil)
        w.Flush()
        panic("etc...")
    }

Or even drop the type name:

    func someFunction(w interface {
        io.Writer
        Flush() error
    }) error {
        w.Write(nil)
        w.Flush()
        panic("etc...")
    }

But maybe that looks too weird.


This is something I'm still struggling to grok. The interface idioms feel so different to my Java instincts, like loose fitting clothes after years of skinny jeans.


Basically, the interfaces get defined where they're used, not where they're implemented.


Golang's interfaces are a half-baked feature, not so different from the rest of the language. What they basically were aiming for is called "structural typing". However, other languages that have that feature have a much better approach, without sacrificing readability, functionality, or discoverability, and ending up causing bugs in the standard library like golang did.


What's worse, defer's function-scoped nature means that the only way to compile it is to dynamically push closures onto a stack at runtime and then pop them off one-by-one before returning. The compiler may be able to optimize this in specific cases, but in the general case the semantics are extremely dynamic for no real benefit. Designing defer in this way is an especially strange decision for a language that in many ways is architected to make life easy for the compiler writer.


I don't understand what you're saying but defer cost a few ns so it's very fast and usually not a performance problem. ( ie: don't use it in hot path )


If I am understanding him correctly: If defer were statically scoped, that is, if it were scoped to curly braces, you would "know" at compile time when and if a defer is going to run (relative to that scope). That is, a defer boils down to just "at the end of this scope, run this code" and nothing more, and all the compiler has to do is move that code/function call to the closing curly brace. (Similar to how a C++ compiler knows when to run a destructor for an object.)

However, by having them be function scoped, this isn't so anymore; e.g, a defer occurring in an if() statement needs to happen at the end of the function, but only if the if occurs. If you loop over a defer, we need to accumulate those. (And the golang tour even explicitly calls the behavior out.[1]) So, instead of just running the defer at the end of the if / inside the if statically, we need to push the defer onto a runtime stack of yet-to-be-run defers that we'll evaluate at the end of the function. This now has to happen at runtime, not compile time, and makes the function compilation more complex, and requires a stack somewhere to push this stuff onto.

From a compiler writer's perspective, I would agree w/ the parent: this seems much more complex, and runs counter to golang's otherwise simple design philosophy.

[1]: https://tour.golang.org/flowcontrol/13


It's not that defer is slow in absolute terms†; it's that it's slower than it needs to be for no good reason. The corresponding C++ feature (RAII) is "free" (as in, it adds no overhead over manually writing the cleanup code), while defer is not. Defer would be easier to use for the programmer, easier for the compiler writer, and faster at runtime if it were block-scoped.

†Though the linked list that the compiler has to generate could hit malloc, which costs more than a few nanoseconds.


I haven't tested this since then, but this wasn't the case in 2014[1]. I haven't noticed these issues since then, but back then it cost hundreds of microseconds -- 8 orders of magnitude more than "a few ns".

EDIT: I just re-did the relevant tests on Go 1.11.5. It's significantly better than in 2014 but it still costs between ~20us and ~50us ("only" 4 orders of magnitude more than "a few ns").

   goos: linux
   goarch: amd64
   BenchmarkPut-8             50000             27502 ns/op
   BenchmarkPutDefer-8        30000             46774 ns/op
   BenchmarkGet-8             50000             29812 ns/op
   BenchmarkGetDefer-8        20000             89701 ns/op
   PASS
[1]: https://lk4d4.darth.io/posts/defer/


It actually did get significantly better in Go 1.8, in early 2017. See https://golang.org/doc/go1.8 ("The overhead of deferred function calls has been reduced by about half.").

However, I think this change was mostly just an optimization for common cases with few arguments or no closures (e.g. "defer f.Close()").


I just re-did the benchmarks with Go 1.11.5, it is much better but it's still 4 orders of magnitude more than "a few nanoseconds".


A common idiom is the defer, for example, file.Close() only if there wasn't an error when the file was opened. You can also put them in an if statement. This means that the deferred function aren't strictly tied to their scope.

Putting the defers on the stack is also part of how the runtime unwinds the stack during a panic.


> Putting the defers on the stack is also part of how the runtime unwinds the stack during a panic.

All exception ABIs on all major platforms can do this without any overhead in the no-exception case. The compiler embeds static metadata (in a subset of DWARF, on Linux) alongside the function, and the unwinder parses that metadata in order to determine which destructors to invoke.


To add on; recover itself is a nice code-smell.

If you're using recover, it's probably time to rewrite the func.

I say this because Go forces you (minus just ignoring with _) to error check; so panic's shouldn't happen to start with.


Well, panic should mostly be happening as a result of nil pointer deref, slice out of bounds, etc. You wouldn't want those types of operations to return error values.


But for those you can do a manual bounds & pointer check.


Your code is allowed to panic if there is a bug. For instance, the caller passed in a nil pointer where there needed to be actual data.

Errors are for when the input (specifically the I/O) is wrong, a precondition isn't met, or some other error that doesn't mean there is something fundamentally wrong with your code.

If to make the problem go away you need to fix the code, panic is OK.


> I say this because Go forces you (minus just ignoring with _) to error check;

   fmt.Println("foo")
Where did golang force you to error check?

How about (taken from here: (https://www.reddit.com/r/programming/comments/ak305l/goodbye...):

    r1, err := fn1()
    r2, err = fn2()
    if err != nil {
      return err
    }


The problem is that it doesn't really force you to do error checks... and really most code I've seen doesn't really handle the error either so much as just bubble it up like a mini-exception.


Are they even aware of it ? I mean like ML discussions about it.


The article is on a suspect domain:

  https://https.www.google.com.tedunangst.com/flak/post/griping-about-go
Is there any legit reason to have https.www.google.com in there?


My AV blocked the website for Phishing. Guessing it's got a bad reputation, or as you say, it's just "suspect".


I think that’s what we call “a joke”.


FYI, you appear to have been shadowbanned for a while.


Looking at his history, it looks like it happened ca. November 2017: https://news.ycombinator.com/threads?id=draw_down&next=15676...

Looking back for a few pages before that, I don't see any moderators warning him. He does seem to post an awful lot of single-sentence replies, so maybe it was automated?

I have my own concerns about the lack of transparency around moderation & banning.


How come we can see his posts if he's been shadow banned?


For the comment in this thread, I used the "vouch" link to reinstate it (requires a certain amount of karma AFAIK), so it's no longer "dead", i.e. hidden. But most of the rest of their comments in the last two weeks are still dead. Anyone can opt in to seeing such posts by turning on "showdead" in their profile options.


Yes, I know but not much I can do about it, thanks


AFAIK you're supposed to email hn@ycombinator.com to negotiate about getting unbanned, unless you've done that already. (Out of curiosity, do you know because someone else told you, or because you were formally notified?)


Automatic closeout remains a hard problem. "Defer" has the same problem as RAII - what if the closeout fails? Python's "with" clause, and how it interacts with exceptions, is one of the few constructs that can handle multiple closeout faults correctly.

Trying to get rid of exceptions seems to force workarounds that are worse than exceptions. C++ and Java exceptions were botched and gave the concept a bad name. Go's "panic" and "recover" are an exception system, but not a good one. Python comes closer to getting it right.

Key concepts for successful exceptions:

- A predefined exception hierarchy. Catch something in the tree, and you get everything below it. Python added this in the 2.x era, and it made exceptions usable. (Then they messed it up in the 3.x era, putting too much stuff under "OSerror".) This solves the problem of "Have I caught everything"?

- The case where a closeout event raises an exception has to work. This is hard. Attempts to get it right resulted in such horrors as "re-animation" in Microsoft Managed C++. It needs something like the Rust borrow checker model to make sure that object lifetimes are properly enforced on all error paths.


Hm. I actually love how Java does exceptions. I think a little better conventions around when to use checked vs unchecked might help but generally I'd rather checked exceptions be the norm than wholely absent.

What do you think is missing? My only ask would be syntax to capture an exception Future/Expression style for smaller use cases.


I think this is what you’re asking for with your last sentence, but I’m not sure so I’ll say it anyway:

My main gripe with Java’s checked exceptions is that there’s no way to bubble up a checked exception from inside a functional interface, because the function signatures differ. For example, if you want to close a bunch of files, you can’t do `files.forEach(file -> file.close())` if the call to close throws IOException — you need to use an iterator (which is longer) or catch and re-throw it as a RuntimeException (which is much longer). Libraries like JOOL can help here, but not completely.

It also doesn’t help that certain methods are declared as throwing exceptions that aren’t actually thrown, such as `ByteArrayOutputStream#close()`.

I used to argue in favour of checked exceptions, but once I switched to Kotlin, I realised that I didn’t actually miss them at all. However, my other favourite language (Rust) uses something a lot closer to checked than unchecked, so maybe I’m just writing different kinds of code on the JVM to let me get away with having this opinion!


Exception hierarchies are hard to get right. Java botched it too, making checked exceptions look bad.

I like Go's use of a single error type in most public API's. Combine with checked exceptions and you would have two kinds of functions: those that can fail and those that can't. It keeps the "what color is your function" problem to a minimum.


Java checked exceptions are based on CLU, Modula-3 and C++ exceptions clauses.

Naturally one just blames the one that actually got famous using them.


Java 7+ has everything you describe for Python 2.5+.


The problem in Java is checked exceptions, which can be frustrating -- either leading to try {} catch{ /* ignored */} or bubbling up the exception (probably preferable, but more work).

IDE support makes this nicer, and Java's tooling is certainly first-rate in my experience.


I try pretty hard to make the close-outs failsafe. It is very difficult to manage errors or recover once you have started closing resources. However, you are then left manually calling file.Flush (for example), which can easily be forgotten.


Yes. That's the argument for "with" type constructs.


I think go has some serious other problems.

Like the billion dollar mistake. Why is this repeated in any new language? It is just plain awful and stupid. This is easily my biggest gripe with the language (apart from missing generics).

Go combines that greatly with the bonkers error handling:

  if err != nil {...}
Half of all go code ever written consists of the line above.

Now combine that with defer (or go routines) returning errors...


Here's one of my favorite gotchas in Go: why does this error?

https://play.golang.org/p/6LTbtuocu5-


It errors because the type of `err` is `error`, not *formatError. That's why you are supposed to return the error interface, instead of a concrete type.

The value ends up being a non-nil interface value that holds nil.

To avoid encountering this issue, return `error`, not something else.

https://play.golang.org/p/jZ5Fa24bbUz


The real issue here is that it is not immediately obvious that this is actually an interface - one expects the second `err` to be *FormatError due to the signature - it happens to implement the error interface so it goes unnoticed.

Shadowing in a different scope allows it to work properly as well.

I admit that I've never actually run in to this issue in practice as I generally return custom errors as `error`, but I can see why it would confuse someone.


Totes.


> To avoid encountering this issue, return `error`, not something else.

But best practice is to return concrete types.

Also, `err` is just an example in this minimal snippet. It can happen with any variable.

Here's a similar snippet: https://play.golang.org/p/WKPey_PL_ht -- Looks impossible to crash. :P

Spoiler: it's a boxed null, not a null.


Best practice is to return concrete types for everything, but error is one of those "special" cases that people tend to return an `error` for, IME.

You can always unwrap it via type assertion, however ugly that may be.


> But best practice is to return concrete types.

You're taking this as an absolute when you shouldn't be. There's plenty of places in the standard library that don't follow this "rule".


> But best practice is to return concrete types.

Except for errors. Use the `error` interface.


nil interfaces is one of the strangest things in language, and after learning Rust after using Go for years, I think not having Option types is the real wart in the language.

I've never really cared about Generics, but having nullable types is a real mistake to me.


> but having nullable types is a real mistake to me.

Nullable types by themselves are fine, the language and the compiler just needs to make handling “is X null?” work the same way as option types do - or better yet: languages were a variable is only in scope if it is not null.


Yeah, nullable types are one thing. Go's null interface is another:

https://play.golang.org/p/N_3BiUOkRJo

It's probably the first and last to have this quirk.


I believe this is a quirk of how interfaces are implemented: https://golang.org/doc/faq#nil_error


I heard it's better now but when I was trying to do grpc I couldn't get this to build:

https://github.com/improbable-eng/grpc-web/tree/master/go/gr...

Basically there was dependency that changed, and it caused it to not build. The maintainer was just pointing fingers at google. I had no idea what to do, but it just scared the crap out of me.


Blog seems to have collapsed under HN weight (link is bad too).

Archive of archive of page: https://app.pagedash.com/p/d5c8c4bf-d88a-470b-a7f3-adb986ccb...


Is there a reason that go's defer is only function-scoped?


As opposed to... entire program scoped?


As opposed to any set of matching curly brackets in C++, i.e. just "scope"-d. Do not know the real reason but my wild guess: for simplicity.


I'm guessing there are some interesting things you can do when the defer list is scoped to the function. Like perhaps this pattern works:

   loop i over whatever {
      defer foo(i)
   }
I.e. a loop's iterations can put things into the defer list, which is then executed when the function exits. Whereas if the defer list were block scoped, then each defer would execute immediately on the termination of its enclosing iteration.

Also, there is problem with conditional defers like:

   if (condition)
      defer whatever
Okay, so that is in the surrounding scope. Now I need to add some piece of logic:

   if (condition) {
      defer whatever
      piece of logic
   }
Now it's scoped to the curly braces and executes immediately after "piece of logic" is done?

In terms of performance, the defer list has to be an actual run-time object; the compiler can't always optimize away the existence of the defer list. If defer is block-scoped and used in numerous nested scopes of the same function, where the compiler isn't able to remove it, then you get multiple defer list instantiations in the function's stack frame, which is a kind of bloat. These have to be initialized to the empty state on each entry into a block. At most defer list to initialize on entry into a function is less time and space overhead.

A more flexible design would be defer to work with named blocks:

  foo: {
      bar: {
          defer h() foo;
          defer g() bar;
          defer f();
      }
  }
h() is deferred to the termination of foo (thus using foo's defer list); likewise g() in relation to bar, and the f() defer is function scoped.


But you can just wrap whatever in a closure to scope your defer as needed. This is simple and works well.


As opposed to per-scope, like variables in C++.


All variables in Go are function-scoped. They are just being consistent with the overall design in this case.


Variables are lexically scoped by blocks: https://golang.org/ref/spec#Blocks



> as opposed to passing large byte slices or strings around. In theory, this should be more efficient

Why would passing a "large" slice be inefficient?? Maybe on x86 due to a lack of registers, but on 64-bit?


> Usually this can be resolved by creating a new function and calling that from the loop. But frequently not. [etc.]

For me this is the appeal of the language; I really prefer things confined and manually scoped rather than having things globally scoped which would cause a lot of debate just around that. You often have to think about what you want to expose and where, but that's a good thing in my opinion.


    for _, f := range fs {
      func() {
        defer f.Close()
      }()
    }


> I mostly like go, but after working with it a bit more I realize there are a few jibs of which the cut I do not like.

What is this supposed to parse to?


I believe the author meant "a few jibs the cut of which I do not like".


> https://www.merriam-webster.com/dictionary/jib

Looking up jib on a dictionary, none of the definitions sound like they make sense in this context at all.

Oh well.



"Like the cut of their jib" means you like it. They tried to invert it in a clever way, badly.


Emphasis on "badly". I hope they don't write code this way.


Defer also encourages the unsafe behavior of not checking error results. You can't propagate errors from it. About the best you can do is wrap whatever function you were deferring in another function, and then panic if there is an error.


You can actually propagate errors from it. It's not pretty but it works:

    func Pants() (rerr error) {
        defer func() {
            if err := doStuff(); err != nil && rerr == nil {
                rerr = err
            }
        }()
        // ...
        return nil
    }
I use this function all the time with `io.Closer` implementations:

    func DeferClose(err *error, closer io.Closer) {
        cerr := closer.Close()
        if *err == nil && cerr != nil {
            *err = cerr
        }
    }

    func Pants() (rerr error) {
        f, _ := os.Open(...)
        defer errtools.DeferClose(&rerr, f)
        // ...
        return nil
    }



#notasuspectURL


What do you mean? I'm a tech-savvy person, so I checked that the lock was green before I submitted my financial credentials.


Doesn't the new version of Chrome have a feature that flags "lookalike" URLs?


[flagged]


You're not serious, right?


>You're not serious, right?

Nope. I like what you like.


I avoid using defer


The other commenter wasn't very kind towards you, but I'd definitely encourage you to use defer to get correct semantics around releasing resources. It's the best tool that the language gives you for that.


Sorry, but for me its not a great method of releasing resources.

I tend to (for example) open a connection in one function, do something with it in another function, close it in another. I'm not writing traditional go programs, i wrap things in an api.


Sounds like that practice would make resource lifetimes hard to reason about. And without defer, you don't have the opportunity to clean up resources if a goroutine panics.


The higher level procedure which uses these three functions could use defer to ensure that the closing function is called.


I would have to re-arrange my call stack so that the read or write functions would be inside the open statement, then it would close it at the end anyway so its kind of pointless for my uses.

I try and totally avoid panics and never throw them unless its at some initial parser stage. But this is made me rethink the structure of how some of my tags operate in my interpreter / transpiler.


That seems dumb; `defer` is an excellent and idiomatic tool for many use cases, principally but not exclusively resource cleanup.




Applications are open for YC Summer 2019

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

Search: