← Back to context

Comment by torginus

1 day ago

I still don't understand why defer works on function scope, and not lexical scope, and nobody has been able to explain to me the reason for it.

In fact this was so surprising to me is that I only found out about it when I wrote code that processed files in a loop, and it started crashing once the list of files got too big, because defer didnt close the handles until the function returned.

When I asked some other Go programmers, they told me to wrap the loop body in an anonymus func and invoke that.

Other than that (and some other niggles), I find Go a pleasant, compact language, with an efficient syntax, that kind of doesn't really encourage people trying to be cute. I started my Go journey rewriting a fairly substantial C# project, and was surprised to learn that despite it having like 10% of the features of C#, the code ended up being smaller. It also encourages performant defaults, like not forcing GC allocation at every turn, very good and built-in support for codegen for stuff like serialization, and no insistence to 'eat the world' like C# does with stuff like ORMs that showcase you can write C# instead of SQL for RDBMS and doing GRPC by annotating C# objects. In Go, you do SQL by writing SQL, and you od GRPC by writing protobuf specs.

So sometimes you want it lexical scope, and sometimes function scope; For example, maybe you open a bunch of files in a loop and need them all open for the rest of the function.

Right now it's function scope; if you need it lexical scope, you can wrap it in a function.

Suppose it were lexical scope and you needed it function scope. Then what do you do?

  • Making it lexical scope would make both of these solvable, and would be clear for anyone reading it.

    You can just introduce a new scope wherever you want with {} in sane languages, to control the required behavior as you wish.

    • You can start a new scope with `{}` in go. If I have a bunch of temp vars I'll declare the final result outside the braces and then do the work inside. But lately days I'll just write a function. It's clearer and easier to test.

    • Currently, you can write

          if (some condition) { defer x() }
      

      When it's lexically scoped, you'd need to add some variable. Not that that happens a lot, but a lexically scoped defer isnt needed often either.

      5 replies →

  • > Suppose it were lexical scope and you needed it function scope. Then what do you do?

    Defer a bulk thing at the function scope level, and append files to an array after opening them.

    • That seems like more work, and less readability, than sticking in the extra function.

      Would be nice to have both options though. Why not a “defer” package?

  • I never wanted function-scope defer, not sure what would be the usecase, but if there was one, you could just do what the other comments suggested.

    • Really? I find the opposite is true. If I need lexical scope then I’d just write, for example

        f.Close() // without defer 
      

      The reason I might want function scope defer is because there might be a lot of different exit points from that function.

      With lexical scope, there’s only three ways to safely jump the scope:

      1. reaching the end of the procedure, in which case you don’t need a defer)

      2. A ‘return’, in which case you’re also exiting the function scope

      3. a ‘break’ or ‘continue’, which admittedly could see the benefit of a lexical scope defer but they’re also generally trivial to break into their own functions; and arguably should be if your code is getting complex enough that you’ve got enough branches to want a defer.

      If Go had other control flows like try/catch, and so on and so forth, then there would be a stronger case for lexical defer. But it’s not really a problem for anyone aside those who are also looking for other features that Go also doesn’t support.

      4 replies →

  • You do what the compiler has to do under the hood: at the top of the function create a list of open files, and have a defer statement that loops over the list closing all of the files. It's really not a complicated construct.

  • defer { close all the files in the collection }

    ?

    • OK, what happens now if you have an error opening one of those files, return an error from inside the for loop, and forget to close the files you'd already opened?

      1 reply →

1. it avoids a level of indentation until you wrap it in a function

2. mechanic is tied to call stack / stack unwinding

3. it feels natural when you're coming from C with `goto fail`

(yes it annoys me when I want to defer in a loop & now that loop body needs to be a function)

  • I think you hit the nail on the head - I think it's the stupid decision on Go lang designers part to make panic-s recover-able. This necessitates stack unwinding, meaning defer-s still need to run if a panic happens down the stack.

    Since they didn't want to have a 'proper' RAII unwinding mechanism, this is the crappy compromise they came up with.

As it is, you can have it both ways. Wrap the body in a function if that's what you want. Don't wrap to get wider scope.

I’ve worked with languages that have both, and find myself wishing I could have function-level defer inside conditionals when I use the block-level languages.

There’s probably no deep reason, does it matter much?

  • Yes it does, function-scope defer needs a dynamic data structure to keep track of pending defers, so its not zero cost.

    It can be also a source of bugs where you hang onto something for longer than intended - considering there's no indication of something that might block in Go, you can acquire a mutex, defer the release, and be surprised when some function call ends up blocking, and your whole program hangs for a second.

    • I think it's only a real issue when you're coming from a language that has different rules. Block-scoping (and thus not being able to e.g. conditionally remove a temp file at the end of a function) would be equally surprising for someone coming from Go.

      But I do definitely agree that the dynamic nature of defer and it not being block-scoped is probably not the best

  • Having to wrap a loop body in a function that's immediately invoked seems like it would make the code harder to read. Especially for a language that prides itself on being "simple" and "straightforward".

You can write SQL or use protbuf spec with C#. You just also have the other options.

Lexical scope does not have a stack to put defer onto.

  • All the defer sites in a lexical scope are static, you can target those sites directly or add a fixed-size stack in the frame.