Hacker News new | ask | show | jobs
by alec 4597 days ago
"The guarantees needed to avoid leaving the server in a bad state when handling panics would be impossible without the defer mechanism Go provides."

I'm only passingly familiar with defer, but I understand it to be equivalent to RAII in C++, Python's with statement, Common Lisp's unwind-protect, and others - does is actually provide something more, and if so, what?

3 comments

Go's "defer" is not equivalent to RAII. It is function-scoped rather than block-scoped and has semantics based on mutating hidden per-function mutable state at runtime. For example:

    func Foo() {
        for i := 0; i < 5; i++ {
            if Something() {
                defer Whatever()
            }
        }

        // ... the compiler can't tell how many
        // Whatever()s run here ...
    }
Compared to RAII as implemented in for example D with its "scope" statement, "defer" has much more complex semantics, inhibits refactoring since moving things to function bodies or inlining function bodies silently changes semantics, and cannot be optimized as easily, because of the dynamic aspects. IMHO, it has essentially no advantages over RAII and many disadvantages.
> defer inhibits refactoring since moving things to function bodies or inlining function bodies silently changes semantics, and cannot be optimized as easily, because of the dynamic aspects.

As someone who has written and reviewed hundreds of thousands of lines of Go code, I haven't observed this to be the case in practice.

RAII doesn't fit into Go, philosophically, as it lets you trigger hidden functionality on the creation or destruction of data structures, whereas a deferred function can only be run if there's a defer statement there in the code (where you can see it).

In Go, the only way to execute a block of code is to make a function call. There are no constructors, destructors, or any other kind of side effect to allocating or deallocating data structures. This brings a huge benefit in terms of readability and transparency.

Anyway, I'm not sure why we're comparing defer and RAII, because they're generally used for different purposes.

> As someone who has written and reviewed hundreds of thousands of lines of Go code, I haven't observed this to be the case in practice.

Sure, a lot of suboptimal design decisions don't cause problems in practice. That doesn't change the fact that they're suboptimal, and in this case lead to worse performance.

> RAII doesn't fit into Go, philosophically, as it lets you trigger hidden functionality on the creation or destruction of data structures, whereas a deferred function can only be run if there's a defer statement there in the code (where you can see it).

I'm focusing more on RAII as implemented with "scope" in D; whether stuff runs explicitly or implicitly is an orthogonal design choice (although I prefer implicitly running code since you need finalizers anyway in any GC'd language, including Go—so you might as well embrace it). With the "scope" statement you also always have to explicitly call the destructor, but in a lexically scoped way.

The main thing I find suboptimal with "defer" is the choice of dynamic mutable state as compared to lexical scoping.

> In Go, the only way to execute a block of code is to make a function call. There are no constructors, destructors, or any other kind of side effect to allocating or deallocating data structures. This brings a huge benefit in terms of readability and transparency.

http://golang.org/pkg/runtime/#SetFinalizer

This appears to be a helper function used exclusively by the standard library to handle file descriptor closing (incidentally, the one issue I've had with Golang's concurrency model).
> This appears to be a helper function used exclusively by the standard library to handle file descriptor closing (incidentally, the one issue I've had with Golang's concurrency model).

But it's part of the public API. You can add a finalizer to any object. The semantics of Go say that finalizers are run automatically when the GC reclaims an object. So this statement is wrong: "In Go, the only way to execute a block of code is to make a function call. There are no constructors, destructors, or any other kind of side effect to allocating or deallocating data structures." It would be more correct to say "idiomatically, in Go people tend to prefer calling functions explicitly, and 'defer' encourages this."

I think the fact that it's used by the standard library to close file descriptors is actually really illustrative: you need finalizers in a GC'd language, otherwise you'll leak resources. Not all resources are stack-scoped. So implicitly running functions on deallocation is a necessary evil. You might as well embrace it in your language design.

It may be part of the "public API" solely because it needs to be made available to several different components of the standard library, which is itself at pains to implement itself primarily in Golang.

SetFinalizer feels like a low blow, here.

Actually, across the Go standard library, "defer" is used pretty much exactly for what 80% of C++ RAII is used for: cleaning up locks.
It's not at all equivalent to RAII, or really any of those other examples; it's a way of clearing up the control flow of a function that needs cleanup work before it returns, but it is a fussy and error-prone way of expressing scoped resource access.

"Defer" is nice to have, and because it does less than scoped acquisition, it's easier to repurpose for other jobs; that's kind of thematic of Golang --- simple, orthogonal advances over C/Java/C++; a distinct lack of "theoretical" ambition.

Deferred statements will run even when the program panics so you can ensure you clean up. It's more like a finally statement in some languages.