5 ms·
> So, for those of you who are willing to explore the part of this that goes beyond a simple rational analysis and criticism of language design, whats bugging y
by oconnor663 5y ago
> So, for those of you who are willing to explore the part of this that goes beyond a simple rational analysis and criticism of language design, whats bugging you?
I coded primarily in Go at work for several years, although that was several years ago. I understand that package management and now generics have changed in major ways since then. But at the risk of being out of date, here are some language-level complaints I had at the time, focusing less on convenience/taste and more on things that I felt made it harder to write correct code:
- Nil pointers. Go doesn't have any built-in way of distinguishing between pointers that can be nil and pointers that can't.
- Two kinds of nil. Go distinguishes between typed and untyped nil pointers.
- Automatic zero-initialization of omitted fields. Adding a new field to a struct has a tendency to make existing callsites incorrect without producing any compiler errors.
- Mixing declaration and assignment on the left side of the := operator. Copy-pasting a line from an outer scope to an inner one silently changes assignments into shadowing declarations.
- Calling append() without capturing its return value is always wrong. This mistake is easy for linters to catch in simple cases, but aliasing creates more complicated cases.
- It's easy to make an implicit temporary copiy of an object without realizing it, like by using a value method receiver or by looping over a slice of values.
These are examples of things that made me feel like I was "fighting the language" to try to write correct code. I'd appreciate feedback about any of these that might have changed in the last few years.
- kaba0 5y agoAnd the stupidest of all: defer is function-scoped, not block.
- oconnor663 5y agoI don't like calling any of these things stupid. They're tradeoffs, and I can respect why they were made.
- kaba0 5y agoI would agree with you in the general case, but I yet to hear any sort of sane explanation for why it is the way - and it is not like a typical tradeoff where something arguably worse would have been chosen otherwise, it is just a small semantic change to a keyword. But its effect is huge, e.g. the prototypical usage of the keyword would be to unlock mutexes, but it is simply a huge footgun, see: https://news.ycombinator.com/item?id=30253426 https://news.ycombinator.com/item?id=30253426
- kevinmgranger 5y agoIt enables one to conditionally set up cleanup within the function. If it was block scoped, you couldn't do something like `if shouldDoSideEffect { defer something() }` If it was scope-based, there would be just as many people complaining that it isn't function based.
- kaba0 5y agoYou could just do `defer if shouldDoSideEffect { something() }`
- kevinmgranger 5y agoThat's slightly different, though. The first is "when this is true, earlier in the function, queue up running something at the end of the function." This is "at the end of the function, if this is true, do something." They _should_ be identical, unless shouldDoSideEffect changes. Yes, mutability is bad. One shouldn't change that. But avoiding "you're holding it wrong" footguns was the original goal, right? Don't get me wrong, having both would be nice. Maybe something like `after` for function scope and `defer` for block scope. But that starts to erode the "simplicity" of Go, I guess.
- oconnor663 5y agoI think that gets unwieldy in more complicated cases. You might want to write: if condition_A { acquire_A() defer release_A() } else if condition_B() { acquire_B() defer release_B() } else { acquire_C() defer release_C() } But with block-scoped defer you'd have to write something like: if condition_A { acquire_A() } else if condition_B { acquire_B() } else { acquire_C() } defer func() { if condition_A { release_A() } else if condition_B { release_B() } else { release_C() } }() Maybe the syntax here could be better with some kind of defer-if added to the language, but it's the non-locality of the cleanup that's the real downside. The whole point of the defer keyword is to let you put cleanup right next to resource acquisition, to make code easier to audit and maintain, and we lose that here. (Also this version might not even be correct, if the conditions change their values over time.) To be honest, I think Rust's combination of destructors and move semantics is really the ideal way of doing cleanup, with C++ coming pretty close too. But given a defer keyword, I can see situations where having it block-scoped would be painful.