Suggestion: Scope Defer

340 views
Skip to first unread message

Henry Adi Sumarto

unread,
Jul 5, 2015, 8:58:30 AM7/5/15
to golan...@googlegroups.com
Hi,

The current implementation of defer lets the deferred function runs when the owning function exits. While this is okay, I think it would be more useful if defer runs when it is out of scope (instead of functions). It would be useful in a scenario such as this :

func DoWork() error{

for a:=0; a<10; a++{
resource := AllocateResource()
defer func(){
//Clean up resource
}
//...
if err:=resource.ErrorProne(); err!= nil{
return error
}
//...
if someWeirdCondition(){
panic("Panicking")
}
}
}

The current implementation of defer does not let you do this. Instead you have to put the clean up codes at the end of the loop and at every possible early terminations from the loop.

Any thoughts?

Axel Wagner

unread,
Jul 5, 2015, 9:05:41 AM7/5/15
to Henry Adi Sumarto, golan...@googlegroups.com
Henry Adi Sumarto <henry.ad...@gmail.com> writes:
> The current implementation of defer lets the deferred function runs
> when the owning function exits. While this is okay, I think it would
> be more useful if defer runs when it is out of scope (instead of
> functions).

While I absolutely and unconditionally agree that this would be an
infinitely better approach to defer, this is sadly a breaking change
that can not be done in go1. You would need to either wait for go2, or
add a separate keywoard (which I don't think is a tradeoff worthwile by
most gophers).

peterGo

unread,
Jul 5, 2015, 9:32:59 AM7/5/15
to golan...@googlegroups.com
Henry,

No.

You can already do this:

func doWork() error {

    resource := AllocateResource()
    defer func() {
        //Clean up resource
    }()
    //...
    if err := resource.ErrorProne(); err != nil {

        return error
    }
    //...
    if someWeirdCondition() {
        panic("Panicking")
    }
}

func DoWork() error {
    for a := 0; a < 10; a++ {
        err := doWork()
    }
}


Peter

Henry Adi Sumarto

unread,
Jul 5, 2015, 9:55:04 AM7/5/15
to golan...@googlegroups.com
I am sure a workaround exists. I have thought of several variations as well. This is not a must-have feature where its absence will leave you stuck in a hopeless situation. In fact, you can also do away without defer if you wish and take defer out from Go. Defer itself is an ergonomic feature and not a critical one.

However, I still think that defer should have run when it is out of scope instead of out of function. It just seem like a better design that way. I don't think that the change will break existing codes, considering that when you go out of function, you also go out of scope. Hence, defer in existing codes should still work.

I hope the Go Team would seriously consider this.

Axel Wagner

unread,
Jul 5, 2015, 10:04:41 AM7/5/15
to Henry Adi Sumarto, golan...@googlegroups.com
Henry Adi Sumarto <henry.ad...@gmail.com> writes:
> However, I still think that defer should have run when it is out of
> scope instead of out of function. It just seem like a better design
> that way. I don't think that the change will break existing codes,
> considering that when you go out of function, you also go out of
> scope. Hence, defer in existing codes should still work.

That's not true. For example:
http://play.golang.org/p/t73qE128mX
would have very different behavior (just to illustrate the issue).

Henry Adi Sumarto

unread,
Jul 5, 2015, 11:08:29 AM7/5/15
to golan...@googlegroups.com
@Axel
It's an unusual way to use defer. It looks like a conditional clean-up to me. If it is used like that, then yes it is going to be a breaking change.

But adding an additional keyword for it looks like an ugly band-aid fix to me. Unless someone figure out something smart, if it were up to me, I would rather have a breaking change now than later when there are many more codes written in Go. Alternatively, we can leave it as is, but it's hard to ignore it knowing that there is a better way.

Axel Wagner

unread,
Jul 5, 2015, 11:41:58 AM7/5/15
to Henry Adi Sumarto, golan...@googlegroups.com
Henry Adi Sumarto <henry.ad...@gmail.com> writes:
> @Axel
> It's an unusual way to use defer. It looks like a conditional clean-up
> to me. If it is used like that, then yes it is going to be a breaking
> change.

*Any* defer in *any* nested scope will behave semantically different,
because instead of at the end of a function it is executed after the
scope ends (which is, after all, the intent). Code like this is
not at all uncommon, for example consider something like this:
http://play.golang.org/p/ta9xOaajl-

> But adding an additional keyword for it looks like an ugly band-aid
> fix to me.

Yes, I agree. It also adds complexity for the language and as such is
subject to relatively strict requirements in regards to usefullness. As
illustrated by Peter, there is a relatively simple workaround
that doesn't require any addition to the language, so I don't see an
added keyword making this tradeoff.

> Unless someone figure out something smart, if it were up to me, I
> would rather have a breaking change now than later when there are many
> more codes written in Go.

But it is not :)
https://golang.org/doc/go1compat
You are, unfortunately, about three years too late for any breaking
changes.

> Alternatively, we can leave it as is, but it's hard to ignore it
> knowing that there is a better way.

I think it's not that hard to ignore. Considering the workaround
proposed by Peter, this seems like a relatively minor uglyness to me.

I hope you won't let something like this from using go :)

Best,

Axel

Peter Kleiweg

unread,
Jul 5, 2015, 12:20:41 PM7/5/15
to golan...@googlegroups.com
You can use an embedded function

Jakob Borg

unread,
Jul 5, 2015, 3:05:09 PM7/5/15
to Henry Adi Sumarto, golan...@googlegroups.com

> On 5 jul 2015, at 15:55, Henry Adi Sumarto <henry.ad...@gmail.com> wrote:
>
> I don't think that the change will break existing codes, considering that when you go out of function, you also go out of scope.

I do this and variants a lot:

func (t *T) Serve() {
if debug {
log.Println(t, "starting")
defer log.Println(t, "exiting")
}
// stuff
}

There are a bunch of other cases where scope-scoped defer would break spectacularly.

//jb

Henry Adi Sumarto

unread,
Jul 5, 2015, 9:48:40 PM7/5/15
to golan...@googlegroups.com
For conditional defer, I prefer to do the following.

resource := AllocateResource()
defer func(){
if conditionA {
} else {
}
}()

It seems clearer that way, and it is easier to notice when you are skimming the codes. Compare that to the one below.

resource := AllocateResource()
if conditionA {
defer...
}else{
defer...
}

But everybody has their own ways of organizing their codes. So it is indeed possible that the change will break existing codes.

Second, in Jakob's example, I don't think putting debugging code into the production code is a good idea, because you end up with different versions of the program. What works in the debug version may not work in the release version. Instead of embedding debug codes, I prefer using test. But everybody has their own ways of doing things.

Lastly, what I don't like in a number of Go's workarounds is that it involves a liberal use of extra functions. When used properly, breaking down codes into functions can indeed improve readability. However, when you use too many of these, it hurts readability. In addition, calling a function has a cost. The cost may be small by today's standard, but when you put this inside a long loop, it may become significant, unless if Go compiler is smart enough to know which of these functions should be inlined. Compared to existing C/C++ compiler, Go compiler is still lagging behind when it comes to optimization.

Nigel Tao

unread,
Jul 5, 2015, 10:05:19 PM7/5/15
to Henry Adi Sumarto, golang-nuts
On Sun, Jul 5, 2015 at 10:58 PM, Henry Adi Sumarto
<henry.ad...@gmail.com> wrote:
> Any thoughts?

The short answer is that we cannot change the meaning of "defer" for
any Go 1.x release: https://golang.org/doc/go1compat

The longer answer is that while there's benefit of a scope-scoped
defer, there's also benefit in a function-scoped defer. This code:

func foo(filename string) error {
var r io.Reader
if filename != "" {
f, err := os.Open(filename)
if err != nil {
return err
}
defer f.Close()
r = f
} else {
r = strings.NewReader(fakeInput)
}
// More code that reads from r.
etc
}

is easier to write with a function-scoped defer than a scope-scoped defer.

Henry Adi Sumarto

unread,
Jul 5, 2015, 10:33:45 PM7/5/15
to golan...@googlegroups.com
@Nigel
Actually, in the example you gave, scoped defer works better. The 'f' variable is local within the 'if'. In the scoped defer, 'f' will be cleaned up upon exiting the 'if'. It works as you intend it to be.

With the function defer, 'f' will be out of scope and defer will execute when 'f' no longer exists.
Message has been deleted
Message has been deleted
Message has been deleted

Henry Adi Sumarto

unread,
Jul 5, 2015, 10:54:00 PM7/5/15
to golan...@googlegroups.com
@Nigel

Sorry. I was replying through my cellphone and didn't read your code properly. I didn't notice the reassignment of 'f' to 'r'. But considering that r is the object being cleaned up, wouldn't it make more sense to move defer right below r?

func foo(filename string) error{
      var r io.Reader
      defer func()[
            //if r is *File, do this
            //else do something else.
      }

     if filename !=""{
          //...
     }else{
         //...
     }

Josh Bleecher Snyder

unread,
Jul 5, 2015, 11:23:57 PM7/5/15
to golang-nuts
> Sorry. I was replying through my cellphone and didn't read your code
> properly. I didn't notice the reassignment of 'f' to 'r'. But considering
> that r is the object being cleaned up, wouldn't it make more sense to move
> defer right below r?

Putting the deferred close right next to the open, as Nigel did, makes
the code obviously correct. Having the defer up at the declaration of
r does not. And if you go to edit the code, you are likely to make the
correct modifications, particularly if the function is long. Locality
matters, particularly for humans.


> In addition, calling a function has a cost. The cost may be small by today's standard, but when you put this inside a long loop, it may become significant, unless if Go compiler is smart enough to know which of these functions should be inlined.

The compiler does not currently inline any functions at all that
include a defer. However, if you consider what is required for
correctness--including having the correct state of memory and local
variables, having the correct tracebacks, running multiple defers,
etc.--I think you would find that any correct implementation of defer
will not be made much cheaper by avoiding the function call overhead.


Another consideration:

Using function scoping for defers is general. You can get any behavior
you want using a closure. However, some scopes are not accessible in
the way you might want for your proposal. For example, consider:

func f() {
// ...
func() {
for i := 0; i < 3; i++ {
x := use(i)
defer cleanup(x)
}
}()
// ...
}

This will clean up everything once the entire for loop completes. The
relevant scope is the entire for loop (the scope at which i is
declared). However, the scope of each call to defer is the scope of
the body of the for loop (the scope at which x is declared). There is
simply nowhere in the body of a for loop to put a defer that executes
at the scope of the entire loop--and yet the calls you want to make to
defer may depend on values calculated in the body of the loop.

-josh
Reply all
Reply to author
Forward
0 new messages