[HN Gopher] Fixing for loops in Go 1.22 ___________________________________________________________________ Fixing for loops in Go 1.22 Author : todsacerdoti Score : 278 points Date : 2023-09-19 19:34 UTC (3 hours ago) (HTM) web link (go.dev) (TXT) w3m dump (go.dev) | assbuttbuttass wrote: | for _, informer := range c.informerMap { informer := | informer go informer.Run(stopCh) } | for _, a := range alarms { a := a go | a.Monitor(b) } | | Not sure what the difference could be, but let me take a guess. | In one case, the loop variable is a pointer, and in the other | case a value. The method call uses a pointer receiver, so in the | value case the compiler automatically inserts a reference to the | receiver? | Spiwux wrote: | I have such a love-hate relationship with this language. I use it | professionally every single day, and every single day there are | moments when I think to myself "this could be solved much more | elegantly in language X" or "I wish Go had this feature." | | Then again I also can't deny that the lack of ""advanced"" | features forces you to keep your code simple, which makes reading | easier. So while I hate writing Go, I like reading unfamiliar Go | code due to a distinct lack of magic. Go code always clearly | spells out what it does. | bvinc wrote: | "Love-hate relationship" were the exact words that I used when | I used go professionally every day. | | I could complain all day about things the language does | obviously wrong, often in the name of simplicity. But after all | my complaints I still admit it's a very good choice for certain | kinds of software and software companies. | [deleted] | JyB wrote: | The first point cannot bother you after you've correctly | realized your second point. The more empathy you have for your | future-self or your peers, the clearer it becomes. | jamespwilliams wrote: | Go is the worst language, except all the others | noelwelsh wrote: | This was also an issue in Javascript (e.g. | https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...) | | It's somewhat amusing to see Go rediscover old ideas in | programming language theory, given the stance against PLT that | the Go developers took in the early years of the language. | stouset wrote: | The entire story of go seems to be learning through repeating | the same mistakes as other languages, one at a time. | | Nil being another big one. Even more impressively they doubled | down on this mistake with _typed_ nils. Even if you explicitly | do a comparison with nil you can still shoot yourself in the | foot because it was a different nil than the one you compared | against. | TwentyPosts wrote: | Wait what? Do you have an example or more information on | this? | Thaxll wrote: | op is confused with nil interface. | gwd wrote: | Basic example: func foo() *bar { | // ... if something_wrong { return | nil; } } var x | interface{} x = bar() if | x != nil { // Dereference x } | | This will crash if `foo()` returns nil, because it's | checking if `x == interface{}(nil)`, which is false. What | you wanted to check was whether `x == *bar{nil}` or one of | the other nil types that implements the interface; which | must be done with `reflect.ValueOf(x).IsNil()`. | | https://github.com/golang/go/issues/30865 | Spiwux wrote: | e.g. var typeA Interface = (*TypeA)(nil) | println(typeA == nil) // false | println(typeA == (*TypeA)(nil)) // true | | Yes really https://go.dev/play/p/sz44kJW8OuT | akira2501 wrote: | Programming languages are an exercise in compromise, not pure | application of theory. Well, except maybe Haskell and it's ilk, | but this should be your expectation of most languages and | generally not a surprise. | noelwelsh wrote: | There's no need for compromise in this case. This issue is | something I learned about in the late 90s / early 2000s when | I started reading about programming language theory. It's | found in introductory textbooks. | FridgeSeal wrote: | I think OP's point is that given the go devs casual disregard | for every development in PL theory and design over the last | 30 years, it's amusing to watch them rediscover half the | issues from scratch. | skybrian wrote: | This meme is commonly repeated on HN, but I think it's | inaccurate or at least greatly exaggerated. | | There's nothing _casual_ about Go 's approach to language | design, and I haven't seen any evidence that they're | _unaware_ of what other languages do. | | I also haven't seen much criticism of languages other than | C++ or Java. | FridgeSeal wrote: | Sum/Product types. Generics/type-parameters. Bizarre | handling of nil in places. Error handling that's like | some deliberately crippled version of a Result<T,E>. The | absolutely unhinged decision about zero-value-defaults | for types. I'm sure other people can think of some more, | but that's the ones I can think of off the top of my | head. | | Non PL theory but related: incorrect implementation of | monotonic clocks, and then refusing to fix it because | "just use google smear time bro". | skybrian wrote: | Everyone who disagrees with you is "unhinged." This is | just name-calling. | bobbylarrybobby wrote: | If they're aware of how other languages either handle | these issues or suffer the consequences of not handling | them, then it sure is odd that they consciously decided | to introduce the same issues into their own language, | only to fix them down the road. | skybrian wrote: | Given Go's success, it seems like fixing certain footguns | _much later_ actually worked out pretty well for them? | That doesn't necessarily mean it was right, but it was | perhaps not as big a deal as some people assume. | doctor_eval wrote: | It's also amusing to see someone talk about JavaScript and PLT | in the same sentence, given the practical origins of JS. | noelwelsh wrote: | It's not making any claims about the quality of design in JS | here. I'm literally not talking about them in the same | sentence for this reason. I'm, instead, merely noting that it | had the same issue. Looks like it was fixed in 2014: | https://nullprogram.com/blog/2014/06/06/ C# also had the same | issue, as noted elsewhere in the comments. | [deleted] | [deleted] | anderskaseorg wrote: | MDN goes into great detail, but the important point is that | JavaScript fixed this with let and const. > a | = []; for (i = 0; i < 3; i++) a.push(() => i); a.map(f => f()) | [ 3, 3, 3 ] > a = []; for (let i = 0; i < 3; i++) | a.push(() => i); a.map(f => f()) [ 0, 1, 2 ] > | a = []; for (i of "abc") a.push(() => i); a.map(f => f()) | [ 'c', 'c', 'c' ] > a = []; for (const i of "abc") | a.push(() => i); a.map(f => f()) [ 'a', 'b', 'c' ] | zeroimpl wrote: | In contrast, Java would have a compile error so long as the | variable was not declared final. | ben0x539 wrote: | But isn't it _completely wild_ that the `for (let i =`... | version works like that? What does the loop 'desugar' into | if written as a while-loop? | biomcgary wrote: | Is this loop variable problem really a theory issue (if so, | does it have a label)? Or, primarily a practical one? Is there | a database of known and historical programming language | problems that are nicely tagged with "theory problem", | "frequent foot-gun", etc? | | Could an LLM coupled with a constraint solver create the | perfect language (for particular domains, e.g., performance)? | Or, just use Rust ;-)? | noelwelsh wrote: | In contrast to the view of many, programming language theory | is very closely intertwined with programming practice. It | both drives practice and reacts to practice. | | In this particular case I imagine the issue was uncovered | some time in the 1970s, which is when lexical scoping came to | the fore in Scheme (in the US) and ML (in Europe). It's a | fairly natural problem to run into if you're either thinking | deeply about the interaction between closures (which capture | environments) and loop constructs, or if you're programming | in a language that supports these features. | philosopher1234 wrote: | What stance against PLT are you referring to? | pseudonom- wrote: | Probably quotes like: | | "It must be familiar, roughly C-like. Programmers working at | Google are early in their careers and are most familiar with | procedural languages, particularly from the C family. The | need to get programmers productive quickly in a new language | means that the language cannot be too radical." | | And not including sum types despite having a sum-type-shaped | hole in the language (`if err != nil`). | | And some of the discussion about "why no generics" seemed | kind of divorced from existing PL knowledge on the topic. | tensor wrote: | These were all intentional tradeoffs though, not any | ignorance of theory. Also, it's pretty rich for someone to | be complaining about Go while referencing Javascript of all | languages. Javascript's design flaws are legendary. And I | mean no disrespect to the creators of Javascript, they had | to deal with some crazy last minute change requests to the | language. | kaba0 wrote: | With all the crazy warts that JS has, it is at least a | lisp-like very dynamic language if you squint (a lot) at | it. Its greatest fault is probably leaving out integers | (I can't even fathom why they decided on that, floats | can't represent properly ints). | | Go is just simply badly designed, relying on hard-coded | functionality a lot. | TwentyPosts wrote: | Ehhh, I see absolutely no evidence that the Go developers | were particularly _aware_ of theory. It really feels more | like they just were used to thinking in terms of C, and | built a language which is kind of like C. | | Go also has some really weird stuff in it, such as named | return values. | | Frankly, the lack of sum types hurts the most. The | language would just be a lot better with a unifying | Result type in the library. And don't give me any of that | "oh, they tried to keep the language simple!" stuff. | | Intuitively, sum types are laughably simple. Everyone | understands "It's one of these possible values, so you | need to check which one it is and then handle that | situation." They are more simple than enums on a | conceptual level! Sum types are just not how | C-programmers think about the world. | scottlamb wrote: | I've written a tiny bit of Go and am aware of the general problem | this solves. I don't get their more subtle examples (the | letsencrypt one or "range c.informerMap" vs "range alarms". | | When you do "for k, v := range someMap", is "v" of the map's | value type (and one binding for the whole loop, copied before | each iteration)? This would explain the problem, but I would have | expected "v" to be a reference into the map, and I couldn't find | the answer in a quick skim of the "For statements with range | clause" in the spec. I'm probably looking in the wrong place | because I touch Go rarely... | | [1] https://go.dev/ref/spec#For_statements | | edit: oh, the answer is in the "code block"-formatted table. | Guess I had banner blindness. "v" is the copied value, not a | reference. I'm surprised! | tedunangst wrote: | If you have a map of string to int, then v is of type int. It's | a value. It's not pointer to int. | scottlamb wrote: | Is the expectation that you simply won't create a | `map[...]expensivetocopyvalue`, but instead always do | `map[...]*expensivetocopyvalue`? | tedunangst wrote: | The expectation is the compiler does what you tell it? If | you want pointers in your map, you can do that too. | Arnavion wrote: | They're asking that, if the programmer wants the map to | store expensivetocopyvalue semantically but also doesn't | want to have iteration generate expensive copies, does | the programmer have to change the map to store | *expensivetocopyvalue instead? | | Anyway I believe the answer is that expensivetocopyvalue | is not a type that exists in golang, because golang's | "copy" operation is always a simple bitwise copy ala C | struct copy / Rust's Copy trait, not like C++ copy ctor / | Rust's Clone trait that can be arbitrarily expensive. | morelisp wrote: | In Go, `expensivetocopyvalue` can still be achieved via | an enormous (e.g. multi-KB/MB) structure (which is most | literally expensive to copy) or something containing a | lot of pointers (which is not really expensive to copy | but will start to pressure the GC). | konart wrote: | No expectations whatsoever. You can use pointer or a | values. It's up to you. | scottlamb wrote: | That's silly. Language constructs and APIs are always | made with expectations for how they're used, stated or | not. You can write code that compiles without | understanding and matching those expectations but it | probably won't be good code. | | I'm asking because I think if it were expected that folks | used large/expensive-to-copy map values, this construct | would return a reference instead of copying. In Rust for | example, the std library's "normal" [1] iterators return | references. | | [1] not those returned by into_* or drain. | konart wrote: | Here an example: https://go.dev/play/p/He0lBEYZJ03 | | Value is always a copy. Either a copy of a struct (in | case of map[T]struct{}) or a copy of a pointer (in case | of map[T]*struct{}) | morelisp wrote: | Returning references to storage within the map would be a | substantial footgun without borrowing. | scottlamb wrote: | Thanks, useful reply. I just realized that myself: | https://news.ycombinator.com/item?id=37576558 | | The peer comments along the lines of "the expecation is | it does what it does" are not so helpful from a | perspective of learning to write code that is in harmony | with the language philosophy. | [deleted] | erik_seaberg wrote: | Go doesn't support pointers to map keys or values. It does | support pointers to array slots, but for-range copies each slot | rather than giving you a pointer to it. | scottlamb wrote: | I suppose that makes sense when I think about it for a bit. | My recent expectations come from work in Rust. There the | language prevents you from mutating a map while holding a | reference into it. Go doesn't have a mechanism to prevent | that (except the one you said, simply not supporting those | references at all). If you had a reference into a map that | was resized because of a subsequent mutation, your reference | would have to keep the whole previous map alive and point to | different memory than a reference acquired since then. Both | seem undesirable. | | With array slots, the same issue is present but is a bit more | explicit because those resizes happen with `mySlice = | append(mySlice, ...)`. | erik_seaberg wrote: | I think the slice append semantics are very error-prone, | and it would have been better if a slice was a shareable | reference to a single mutable thing, like a map (or a list | from Python or Java or ...) | parhamn wrote: | > as a consequence of our forward compatibility work, Go 1.21 | will not attempt to compile code that declares go 1.22 or later. | We included a special case with the same effect in the point | releases Go 1.20.8 and Go 1.19.13, so when Go 1.22 is released, | code written depending on the new semantics will never be | compiled with the old semantics, unless people are using very | old, unsupported Go versions | | How does this work? If I pull in a package that decided to pin | 1.22 (as they should) and I compile with 1.18, would it compile | or error that I need to use the 1.22 compiler? | yankput wrote: | They way I understand this, with go 1.18, 1.22 module as a | dependency will compile and produce errorneous logic (!!) if it | depends on this feature | | Thus it will be actively dangerous using go 1.18. I understand | it like that. | | With go 1.19, you will get a compiler error. | | But as go is not fixing security bugs in old releases and std | library, I think it is dangerous to use them anyway. | jerf wrote: | "But as go is not fixing security bugs in old releases and | std library, I think it is dangerous to use them anyway." | | A bit of a harsh way to phrase that. In my experience, the | backwards compatibility promises have been very good, and the | way you stay up-to-date with security fixes and bugs in the | standard library is to upgrade Go. | | I know that may strike terror in the hearts of developers | used to the nightmare that major version upgrades can be in | other languages, where a major version upgrade gets a multi- | week task added into the task tracker, but it's completely | routine for me to upgrade across Go major versions just to | get some particular fix or to play with a new feature. I | expect it to be a roughly five minute task, routinely. | | The only thing that has bitten me about it is arguably not | even Go's fault, which is its continuing advances in TLS | security and the increasing fussiness with which it treats | things connecting with old-style certificates. I can't even | necessarily disagree... I would also like to upgrade them but | while it's my server, the clients connecting to it are using | certs that are not mine and it's out of my control. | yankput wrote: | > A bit of a harsh way to phrase that. In my experience, | the backwards compatibility promises have been very good, | and the way you stay up-to-date with security fixes and | bugs in the standard library is to upgrade Go. | | I don't think we disagree? There is no reason to use old | version of go. | | I speak about grandparent comment who wanted to still run | go1.18. It is not a good idea to still run go1.18, as it | doesn't get security updates. | packetlost wrote: | Yes. | jchw wrote: | Interestingly, though, if you use Go 1.21 and a module declares | a later version of Go, the default behavior is actually to go | fetch a newer toolchain and use it instead[1]. It's a pretty | cool feature, but I am a bit on the fence due to the fact that | it is surprising and phones home to Google-controlled servers | to fetch the binaries. That and the module proxy are for sure | two of the most conflicting features in Go and I'd feel a lot | better about them if Go was controlled by a foundation that | Google merely had a stake in. Alas. | | edit: Actually, though, I just realized what I am talking about | is different than what you and your quote is talking about, | which is what happens when you have a dependency that declares | a different version, not the current module. Oops. | | [1]: https://go.dev/blog/toolchain | tedunangst wrote: | You should get a compile error. | | But your code, when compiled with go 1.22, will still have go | 1.18 semantics. | krackers wrote: | I think https://eli.thegreenplace.net/2019/go-internals- | capturing-lo... describes the problem in more detail? | davidw wrote: | That's a much better explanation for someone like me who isn't | very familiar with Go. | msteffen wrote: | This post was great--thanks for posting it! | | Interestingly, the reason the old i := i trick works is not at | all what I thought! | | The trick, for reference: for i := 0; i < 5; | i++ { i := i // the trick go func() { | print(i) }() } | | What I assumed happened: | | - The escape analyzer sees that the new `i` is passed to a | goroutine, so it is marked as escaping its lexical scope | | - Because it's escaping its lexical scope, the allocator | allocates it on the heap | | - One heap allocation is done per loop iteration, and even | though the new `i` is captured by reference, each goro holds a | unique reference to a unique memory location | | What actually happens: | | - Go's compiler has heuristics to decide whether to capture by | reference or by value. One component of the heuristic is that | values that aren't updated after initialization are captured by | value | | - The new i is scoped to the for loop body, and is not updated | by the for loop itself. Therefore it's identified as a value | that isn't updated after initialization | | - As a result, the Go compiler generates code that captures `i` | by value instead of by reference. No heap allocations or | anything like that are done. | | I recognize that the latter behavior is better, but if anyone | with intimate knowledge of Go knows why the former doesn't | (also) happen (is that even how Go works?) I would love to find | out! | xiaq wrote: | I don't think it's so complex. Without i := i, there's only | one i. With i := i, there's one i per iteration. | | Closure captures are always by reference. | | Heap vs stack allocation don't affect the language semantics. | [deleted] | MatthiasPortzel wrote: | JavaScript had a very similar problem. If the loop variable is | declared with the old `var` then it will not capture the | variable. "New-style" variables declared with `let` are scoped | to the loop. Although, I have to point JS started talking about | making this change almost 20 years ago. As a JS developer, it's | surprising to me to see Go having to make this change now. | wheelerof4te wrote: | Go never should have implemented closures. | | You add them and the next thing people want are objects. | bagful wrote: | All you really need in an language is the former - closures | provide encapsulation (instance variables become bound | variables) and polymorphism (over closures with the same | function signature) all the same | kubb wrote: | Russ Cox and the Go team learned that the loop variable capture | semantics are flawed not by reflecting about how their language | works, but through user feedback. | | This could have been prevented by having one person on the team | with actual language design experience, who could point this | issue out in the design process. | | In this case, after 10 or so years, and thousands of production | bugs, they backpedaled. How many other badly designed features | exist in the language, and are simply not being acknowledged? | | If you point it out, and you're right, will you be heard if you | don't have a flashy metric to point to, like a billion dollars | lost? | | What if the flaw is more subtle, and explaining why it's bad is | harder than in this very simple case, that can be illustrated | with 5 lines of code? What if the link between it and its | consequences isn't that clear, but the consequences are just as | grave? Will it ever get fixed? | AaronFriel wrote: | Many languages have made this mistake, despite having engineers | and teams with many decades or centuries of total experience | working on programming languages. Almost all languages have the | loop variable semantics Go chose: C/C++, Java, C# (until 5.0), | JavaScript (when using `var`), Python. Honestly: are there any | C-like, imperative languages with for loops, that _don't_ | behave like this? | | That decision only becomes painful when capturing variables by | reference becomes cheap and common; that is, when languages | introduce lightweight closures (aka lambdas, anonymous | functions, ...). Then the semantics of a for loop subtly | change. Language designers have frequently implemented | lightweight closures before realizing the risk, and then must | make a difficult choice of whether to take a painful breaking | change. | | The Go team can be persuaded, it's just a tall order. And give | them credit where credit is due: this is genuinely a | significant, breaking change. It's the right change, but it's | not an easy decision to make more than a decade into a | language's usage. | | That said, there may be a kernel of truth to what you're | alluding to: that the Go team can be hard to persuade and has | taken some principled (I would argue, wrong) positions. I'm | tracking several Go bugs myself where I believe the Go standard | library behaves incorrectly. But I don't think this situation | is the right one to make this argument. | tuetuopay wrote: | I hate to be _that_ guy but this would not be possible with | rust, as the captured reference could not escape the loop | scope. Either copy the value, or get yelled at the lifetime | of the reference. | | This is one of the things the language was designed to fix, | by people that looked at the past 50 years or so of | programming languages, and decided to fix the sorest pain | points. | jeremyjh wrote: | C# made the mistake not when they introduced loops, but when | they introduced closures, and it didn't become evident until | other features came along that propelled adoption of | closures. Go had closures from the beginning and they were | always central to the language design. C# fixed that mistake | before the 1.0 release of Go. But the Go team didn't learn | from it. | MatthiasPortzel wrote: | > JavaScript (unless using `for...of`) | | The change in Javscript doesn't have anything to do with | for...of, it's the difference between `var` and `let`. And JS | made the decision to move to `let` because the semantics made | more sense before Go was even created (although code and | browsers didn't update for another several years). That's why | Go is being held to a higher standard, because it's 10+ years | newer than the other languages you mentioned. | AaronFriel wrote: | Ah, thanks for the correction! That's right. But: | | 1. Did JS introduce this change after Go's creation? Yes. | (And also after C#.) | | 2. Did arrow functions support precede let and const | support? Yes. | | Answering the second question and finding the versions with | support and their release dates answers the first question. | Arrow functions let and const Firefox 22 | (2013) 44 (2016) Chrome 45 (2015) | 49 (2016) Node.js 4 (2015) 6 (2016) | Safari 10 (2016) 10 (2016) | | This places it nearly 10 years after the creation of Go. | And with the exception of Safari, arrow functions were | available for months to years prior to let and const. | | This is somewhat weak evidence for the thesis though; these | features were part of the same specification (ES6/ES2015), | but to understand the origin of "let" we also need to look | at the proliferation of alternative languages such as | Coffeescript. A fuller history of the JavaScript feature, | and maybe some of the TC39 meeting minutes, might help us | understand the order of operations here. | | (I'd be remiss not to observe that this is almost an | accident of "let" as well, there's no intrinsic reason it | must behave like this in a loop, and some browsers chose to | make "var" behave like "let". Let and const were originally | introduced, I believe, to implement lexical scoping, not to | change loop variable hoisting.) | adra wrote: | This isn't a bug in java. Java has the idea of "effectively | final" variables, and only final or effectively final values | are allowed to be passed into lambdas seemingly to avoid this | specific defect. Ironically, I just had a review the other | day that touched on this go "interaction". | | The outcome of this go code in java would be as you'd expect, | each lambda generated uses a unique copy of the loop | reference value. | AaronFriel wrote: | Oh, today I learned. I think this was an issue in Scala | (with `var`), but this seems like a great compromise for | Java core. | | I suppose Java had many years after C#'s introduction of | closures to reflect on what went well and what did not. Go, | created in 2007, predates both languages having lightweight | closures. Not surprising that they made the decision they | did. | | Your comment inspired me to ask what Rust does in this | situation, but of course, they've opted for both a | different "for" loop construct, but even if they hadn't, | the borrow checker enforces a similar requirement as Java's | effectively final lambda limitation. | ncann wrote: | Newcomers to Java usually dislike the "Variable used in | lambda expression should be final or effectively final" | compiler error, but once you understand why that | restriction is in place and what happens in other | languages when there's no such restriction, you start to | love the subtle genius in how Java did it this way. | Macha wrote: | Go, designed between 2007 and 2009, certainly had the | opportunity to look at their introduction in C# 2.0, | released 2005, or its syntactic sugar added in C# 3.0, | released 2007. | AaronFriel wrote: | I think that's an ahistorical reading of events. They did | have the opportunity, but there were very few languages | doing what Go was at the time it was designed. My | recollection of the C# 3 to 5 and .NET 3 to 4.5 is a bit | muddled, but it looks like the spec supports a different | reading: | | C# 3.0 in 2007 introduced arrow syntax. I believe this | was primarily to support LINQ, and so users were | typically creating closures as arguments to IEnumerable | methods, not in a loop. | | C# 4.0 in 2010 introduced Task<T> (by virtue of .NET 4), | and with this it became much more likely users would | create a closure in a loop. That's how users would add | tasks to the task pool, after all, from a for loop. | | C# 5.0 in 2012 fixes loop variable behavior. | | I think the thesis I have is sound: language designers | did not predict how loops and lightweight closures would | interact to create error-prone code until (by and large) | users encountered these issues. | omoikane wrote: | This bug appears to be because Go captures loop variables by | reference, but C++ captures are by copy[1] unless user | explicitly asked for reference (`&variable`). It seems like | the same bug would be visually more obvious in C++. | | [1] https://eel.is/c++draft/expr.prim.lambda.capture#10 | scottlamb wrote: | Others have suggested that Rob Pike and Ken Thompson have some | language design experience, to state it mildly. I also want to | point out... | | > Russ Cox and the Go team learned that the loop variable | capture semantics are flawed not by reflecting about how their | language works, but through user feedback. | | I think "user feedback" isn't the whole story. It's not just | the Go team passively listening as users point out obvious | flaws. I've noticed in other changes (e.g. the monotonic time | change [1]) the Go team has done a pretty disciplined study of | user code in Google's monorepo and on github. That's mentioned | in this case too. This is a good practice, not evidence of | failure. | | [1] | https://go.googlesource.com/proposal/+/master/design/12914-m... | kaba0 wrote: | > Rob Pike and Ken Thompson | | They are huge names in the field, but honestly, they just | suck at language design itself. | badrequest wrote: | Are you suggesting Rob Pike has no experience designing a | programming language? | ziyao_w wrote: | I think the parent was trying to imply that Ken Thompson had | no experience in designing a programming language :-) | | Seriously though, "having experience" and "getting things | right" are two different things, although Golang got a lot of | things right, and the parent is being unnecessarily harsh. | ranting-moth wrote: | Or Ken Thompson? | kubb wrote: | I should say language design knowledge. | randomdata wrote: | _> Russ Cox and the Go team learned that the loop variable | capture semantics are flawed not by reflecting about how their | language works, but through user feedback._ | | Since "Go 1" was deemed complete and the "Go 2" project began | in 2018, the direction of the language was given to the | community. It is no longer the Go team's place to shove | whatever they think into the language. It has to be what the | community wants, and that feedback showed it is what they want. | jacquesm wrote: | > This could have been prevented by having one person on the | team with actual language design experience | | I thought you were serious, right up to that bit. Well played. | I hope. | alecbz wrote: | > This could have been prevented by having one person on the | team with actual language design experience, who could point | this issue out in the design process. | | Instead of making a mistake, they could have simply not. | | See also RFC 9225: Software Defects Considered Harmful | https://www.rfc-editor.org/rfc/rfc9225.html | akira2501 wrote: | > How many other badly designed features exist in the language, | and are simply not being acknowledged? | | Very few. | | > If you point it out, and you're right, will you be heard if | you don't have a flashy metric to point to, like a billion | dollars lost? | | If you're right yet don't have a better idea then what do you | expect to occur? | | > What if the link between it and its consequences isn't that | clear, but the consequences are just as grave? | | The consequence is your developers must be careful with loop | variables or they will introduce bugs. That's not particularly | "grave" nor even especially novel. | | I'll admit, it's not a good ivory tower language, but then | again, that's probably why I use it so often. It gets the job | done and it doesn't waste my time with useless hypothetical | features. | [deleted] | amomchilov wrote: | I can't find it now, but I remember some joke about "it's an | interesting language, but why did you ignore the last 50 years | of programming language design?" | | I find Go quite frustrating in how it decries how over- | complicated some features are, and slowly comes around to | realize that oh, maybe people designed them for a reason (who | woulda thunk it?). | Patrickmi wrote: | So whats your point ?, old ideas never die ?, language design | is not language purpose and goal ?, they made a mistake | creating Go ?, refusing to find something suitable or just | break compatibility? | rsc wrote: | I answered some of this above: | https://news.ycombinator.com/item?id=37577312 | dangoodmanUT wrote: | Thank god!!! | raydiatian wrote: | Can somebody please explain to me why this doesn't constitute a | major version due to a breaking change? Maybe I didn't read | precisely enough but it sure sounds like a breaking semantic, | esp. with the fact that "this will only work for versions 1.22 | and later." Sounds like a version upgrade trap to me? What am I | missing? | | Or is it just because it's Golang and they're "we'll never | release a go v2 even if we actually do release go v2 and call it | v1.x" | ben0x539 wrote: | Seems like a pragmatic decision where the breakyness of the | change is mitigated by the module versioning thing. Old code | gets the old behavior, code written in newly created or updated | modules gets the new behavior. Everybody is happy, compared to | the alternative where this ships in a mythical go v2 which | nobody uses while this sort of bug keeps sneaking people's | actual work. | fyzix wrote: | This will result in more memory allocations but it's well worth | it. | orblivion wrote: | For migrating, I wonder if there are any tools that could, let's | say, go through your codebase and add a "// TODO - check" to | every place that might be affected. | [deleted] | tgv wrote: | I think there's a linter for it, perhaps more than one, in | golangci-lint. It might be exportloopref and/or loopclosure. | AaronFriel wrote: | The C# language team encountered this as well, after introducing | lightweight closures in C# 4.0 it quickly became apparent that | this was a footgun. Users almost always used loop variables | incorrectly, and C# 5.0 made the breaking change. | | Eric Lippert has a wonderful blog on the "why" from their | perspective: https://ericlippert.com/2009/11/12/closing-over-the- | loop-var... | | I had a bit of trouble finding the original C# 5 announcement; | that's hopefully not been lost in the (several?) blog migrations | on the Microsoft domain since 2012. | hinkley wrote: | Java also had this problem with anonymous classes. The solution | is usually to introduce a functor. Being pass-by-value, it | captures the state of the variables at its call time, which | helps remove some ambiguity in your code. | | If you try to do something weird with variable capture, then | any collections you accumulate data into (eg, for turning an | array into a map), will behave differently than any declared | variables. | | Go is trying to thread the needle by only having loop counters | work this way. But that still means that some variables act | weird (it's just a variable that tends to act weird anyway). | And I wonder what happens when you define multiple loop | variables, which people often do when there will be custom | scanning of the inputs. | [deleted] | nerdponx wrote: | Meanwhile Python has received this same feature request many | times over the years, and the answer is always that it would | break existing code for little major benefit | https://discuss.python.org/t/make-lambdas-proper-closures/10... | | Given how much of an uproar there was over changing the string | type in the Python 2 -> 3 transition, I can't imagine this | change would ever end up in Python before a 4.0. | | Cue someone arguing about how bad Python is because it won't | fix these things, and then arguing about how bad Python is | because their scripts from 2003 stopped working... | travisd wrote: | It's worth noting that it's much less of a problem in Python | due to the lack of ergonomic closures/lambdas. You have to | construct rather esoteric looking code for it to be a | problem. add_n = [] for n in | range(10): add_n.append(lambda x: x + n) | add_n[9](10) # 19 add_n[0](10) # 19 | | This isn't to say it's *not* a footgun (and it has bit me in | Python before), but it's much worse in Go due to the | idiomatic use of goroutines in a loop: for | i := 0; i < 10; i++ { go func() { | fmt.Printf("num: %d\n", i) }() } | hinkley wrote: | Everyone else solved this problem by using list | comprehensions instead. Rob has surely heard of those. | sneak wrote: | Somehow, Go managed to not break old code and also fix the | problem. | | I think this is a good case of Python not fixing things, | given that a fix exists that solves both problems. | wrboyce wrote: | By letting you specify a language version requirement? Not | exactly backwards compatible (because it is explicitly not, | as per the article). | | Python doesn't make breaking changes in non-major versions, | so as mentioned by the upthread comment the appropriate | place for this change would be in Python 4. | | Given the above, I'm really not sure what point you think | you're making in that final paragraph. | carbotaniuman wrote: | This seems weird to given the number of breakages and | standard library changes I seem to run into every | version. | wrboyce wrote: | Really? I find that surprising. I don't write as much | code as I used to but I've been writing Python for a long | time and the only standard library breakages that come to | mind were during the infamous 2 -> 3 days. | | What sort of problems are have you faced upgrading minor | versions? | pcl wrote: | _> To ensure backwards compatibility with existing code, | the new semantics will only apply in packages contained in | modules that declare go 1.22 or later in their go.mod | files._ | IshKebab wrote: | Python could very easily have a similar mechanism. Hell | even CMake manages to do this right, and they got "if" | wrong. | | The Python devs sometimes seem stubbornly attached to | bugs. Another one: to reliably get Python 3 on Linux and | Mac you have to run `python3`. But on Windows there's no | `python3.exe`. | | Will they add one? Hell no. It might confuse people or | something. | | Except... if you install Python from the Microsoft Store | it _does_ have `python3.exe`. | wrboyce wrote: | I've not run "python3" in years on my Mac, and I'm almost | certain I never type it into Linux machines either; | either I'm losing my mind, or there are some ludicrous | takes in this thread. | rat9988 wrote: | You are surely losing your mind then. Python3 isn't | something esoteric. | wrboyce wrote: | Entirely possible, but my point was I just type "python" | and Python 3 happens. Do modern OS even come with Python | 2 anymore? | | I'm not claiming any mystery about Python, just disputing | how the modern version is invoked. | nerdponx wrote: | Right, and Go has the luxury of being a compiler that | generates reasonably portable binaries, while Python | requires the presence of an interpreter on the system at | run time. | orbisvicis wrote: | Is it a bug? I've always depended on late-binding closures | and I think even recently in a for loop, not that I'm going | to go digging. You can do neat things with multiple functions | sharing the same closure. If you don't want the behavior bind | the variable to a new name in a new scope. From the post I | get the sense that this is more problematic for languages | with pointers. | tester756 wrote: | It is crazy that such behaviour even gets deployed | | It is so unintuitive... | wahern wrote: | It's unintuitive to users of the language, but it's very | intuitive from the perspective of those implementing the | language. Everybody seems to make this mistake. Lua 5.0 | (2003) made this mistake, but they fixed it in Lua 5.1 | (2006). (Lua 5.0 was the first version with full lexical | scoping.) | tester756 wrote: | >It's unintuitive to users of the language, but it's very | intuitive from the perspective of those implementing the | language. | | It sounds like a lack of dogfooding, lack of review? | catach wrote: | To the degree the the implementers are also users they | carry their implementer understanding into their use. | Dogfooding doesn't help when your understanding doesn't | match that of your users. | gtowey wrote: | It's not crazy. It's just the difference between a pointer | and a value, which is like comp sci 101. | | I think the main things that make it such a trap is that the | variable type definition is implicit so the fact that it's a | pointer becomes a bit hidden, and that easy concurrency means | the value is evaluated outside of the loop execution more | often. | tester756 wrote: | >It's not crazy. | | No. Full disagree. | | Array represents a concept of holding multiple values | (let's simplify) of the same type. | | Loop (not index based) over array represents concept of | going *over* array's elements and executing some code body | for each of it. | | Now, if the behaviour isn't that loop's body is executed | for each array element (let's forget about returns, breaks, | etc) | | then the design is terrible (or implementation, but that'd | mean that it was a bug) | | I have totally no idea how can you design this thing in | such a unintuitive way unless by mistake/accidentally. | jrockway wrote: | The loop semantics do not have anything to do with | arrays. The point of confusion is whether a new slot for | data is being created before each iteration, or whether | the same slot is being used for each iteration. It turns | out that the same slot is being used. The Go code itself | is clear `for i := 0; i < 10; i++`. `i := 0` is where you | declare i. Nothing else would imply that space is being | allocated for each iteration; the first clause of the | (;;) statement is only run before the loop. So you're | using the same i for every iteration. This is surprising | despite how explicit it is; programmers expect that a new | slot is being allocated to store each value of i, so they | take the address to it, and are surprised when it's at | the same address every iteration. (Sugar like `for i, x | := range xs` is even more confusing. The := strongly | implies creating a new i and x, but it's not doing that!) | | Basically, here are two pseudocode implementations. This | is what currently happens: i = | malloc(sizeof(int)) *i = 0 loop: | <code> *i = *i + 1 goto loop if *i < 10 | | This is what people expect: secret = | malloc(sizeof(int)) *secret = 0 loop: | i = malloc(sizeof(int)) *i = *secret | <code> *secret = *secret + 1 goto loop | if *secret < 10 | | You can see that they are not crazy for picking the first | implementation; it's less instructions and less code, AND | the for loop is pretty much exactly implementing what | you're typing in. It's just so easy to forget what you're | actually saying that most languages are choosing to do | something like the second example (though no doubt, not | allocating 8 bytes of memory for each iteration). | | Remember, simple cases work: for i := | 0; i < 10; i++ { fmt.Println(i) // 0 1 2 3 4 | ... } | | It's the tricky cases that are tricky: | var is []*int for i := 0; i < 10; i++ { | is = append(is, &i) } for _, i := range | is { fmt.Println(*i) // 9 9 9 9 9 ... | } | | If you really think about it, the second example is | exactly what you're asking for. You declared i into | existence once. Of course its address isn't going to | change every iteration. | hinkley wrote: | If I understood the example, Java had this same problem. | I'm wondering if C# does as well. | mik1998 wrote: | I don't know much about Go but the design seems very | intuitive to me. You're doing something like (let ((i | variable)) (loop (setf i ...) ...body)), which if there | is a closure in the loop body will capture the variable i | and also subsequent mutations. | | The fix is to make a new variable for each iteration, | which is less obvious implementation wise but as per the | post works better if you're enclosing over the loop | variable. | jerf wrote: | jaredpar on the C# team offered the very first comment on the | Github issue for this proposal: | https://github.com/golang/go/discussions/56010 | | I think it played a large part in helping get past the default- | deny that any language change proposal should have. The other | big one for me was the scan done over the open source code base | and the balance of bugs fixed versus created. | em-bee wrote: | as soon as i saw mention of c# going through the same thing, | i realized that this was discussed before: | https://news.ycombinator.com/item?id=33160236 | nzoschke wrote: | Thank you Go team and project! | | Go continues to be my favorite language and experience to build | and maintain in. | | They got so much right from the start, then have managed to make | consistent well reasoned, meaningful and safe improvements to the | language over the years. | | It's not perfect, nothing is, but the "cost" of maintaining old | code is so much lower compared to pretty much every other | language I have used. | devjab wrote: | Go is such a productive language to work with, it's absolutely | mind blowing how little adoption it has around where I live. | Well I guess Lunar went from node to java to go, and harvested | insane benefits from it, but a lot of places have issues moving | into new languages. Not that I think that you should | necessarily swap to a new hipster tech, I really don't, but Go | is really the first language we've worked with that competes | with Python as far as productivity goes. At least in my | experience. | | We'll likely continue using Typescript as our main language for | a while since we're a small team and it lets us share resources | better, but we're definitely keeping an eye on Go. | rcv wrote: | I typically develop in Python, C++, and Typescript, and | recently had to implement some code in Go. So far I've found | it a pretty unpleasant language to use. It feels pedantic | when I don't need it to be, and yet I have to deal with | `interface{}` all over the place. Simple things that would be | a one-liner Python or TS (or even just an std::algorithm and | a lambda in C++) feel like pulling teeth to me in Go. | | I'd love to hear of any resources that can help me understand | the Zen of Go, because so far I just don't get it. | vineyardmike wrote: | One of the big things that I've found helped is to "stop | being an architect". Basically defer abstraction more. | | People, esp from a Java-esque class based world want class | inheritance and generics and all that jazz. I've found at | work like 50% of methods and logic that has some sort of | generic/superclass/OOP style abstraction feature only ever | has 1 implemented type. Just use that type and when the | second one shows up... then try to make some sort of | abstraction. | | For context, I can't remember the last time that I actually | used "interface{}". Actual interfaces are cheap in go, so | you can define the interface at use-time and pretty cheaply | add the methods (or a wrapper) if needed. | | If you're actually doing abstract algorithms and stuff | every day at work... you're in the minority so I don't know | but all the CRUD type services are pretty ergonomic when | you realize YAGNI when it comes to those extra | abstractions. | | Edit: also f** one liners. Make it 2 or three lines. It's | ok. | badrequest wrote: | I write Go every day, and can count the number of times per | year I have to involve an `interface{}` literal on one | hand. Unless you're doing JSON wrong or working with an API | that simply doesn't care about returning consistently | structured data, I can't fathom why you'd be using it "all | over the place." | coffeebeqn wrote: | Me too. We have around a dozen go services and I have | maybe used or seen interface{} once or twice for a hack. | Especially after generics. I think the parent comment is | suffering from poor quality go code. It's like | complaining about typescript because things in your | codebase don't have types | Spiwux wrote: | You discovered the Zen of Go. There are no magic one | liners. It's boring, explicit and procedural. | | Proponents argue that this forced simplicity enhances | productivity on a larger organisational scale when you take | things such as onboarding into account. | | I'm not sure if that is true. I also think a senior Python | / Java / etc resource is going to be more productive than a | senior Go resource. | goatlover wrote: | Go seems like the antithesis to Lisp. | pharmakom wrote: | ... so the code ends up being really long then. | Spiwux wrote: | Yes, pretty much. It's a pain to write, but easy to read. | On a larger scale the average engineer likely spends more | time reading code than writing code | tuetuopay wrote: | I don't find go that easy to read. It is so verbose that | the actual business logic ends up buried in a lot of | boilerplate code. Maybe I'm bad at reading code, but it | ends up being a lot of text to read for very little | information. | | Like a one-line list comprehension to transform a | collection is suddenly four lines of go: allocation, loop | iteration, and append (don't even start me on the append | function). I don't care about those housekeeping details. | Let me read the business logic. | pharmakom wrote: | I think that shorter code is easier to read - to a point! | on balance most code is too long, not too short. | gtowey wrote: | Go is the language that's not made for you, it's made to | make the life of the next guy who has to maintain your code | easier! :-) | hagbarth wrote: | One of the reasons I like Go is that it really doesn't try to | be a hipster language. It's kinda boring, which is great! | Philip-J-Fry wrote: | Boring is good when you want to build things that are | maintainable by 100s of devs. | | Something we have experienced over and over is that devs | moving from languages like C# or Java just love how easy | and straight forwarding developing in Go is. They pick it | up in a week or two, the tool chain is just so simple, | there's no arguing around what languages features we can | and can't use. | | Almost everyone I've spoke to finds it incredibly | productive. These people want to be delivering features and | products and it makes it easy for them to do so. | VirusNewbie wrote: | Maybe a 100 devs Go is fine, but it gets to be a | nightmare as you scale beyond that. | | Language abstractions exist to prevent having developers | build their own ad-hoc abstractions, and you find this | time and time again in languages like Go. You can read | the Kubernetes code and see what I mean, they go out of | their way to work around some of the missing language | features. | [deleted] | kaba0 wrote: | > They got so much right from the start, then have managed to | make consistent well reasoned, meaningful and safe improvements | to the language over the years | | In which universe? They have to constantly patch the language | up and go back on previous assumptions. | christophilus wrote: | Fast compiler, simple tooling, baked in fmt, simple cross | platform compilation, decent standard library, a tendency | towards good enough performance if doing things the Go way, | async without function coloring. They got some things right | and some things wrong. When tossing out orthodoxy, you'll | tend to get some things wrong. I think a lack of sum types is | my biggest gripe. | nzoschke wrote: | The std library is a big part of the magic. It's so | shocking to go to JS land and see that there are 10 | different 3rd party libraries to make http requests, all | with wildly different ergonomics all within one code base | due to cross dependency heck. | | In Go there's pretty much only the http package, and any | 3rd party packages extend it and have the same ergonomics. | | For a while my biggest gripe was package management but | it's a dream where we are now. | 13415 wrote: | That's the _first_ language change that can in theory break | programs (in practice, it won 't). Everything else was just | additions to the existing language with full backwards | compatibility. That's the opposite of constantly patching the | language up. | kaba0 wrote: | You can patch things up without breaking backwards | compatibility. | | But, going on a well-trodden path slower than the pioneers | is not a big achievement. | badrequest wrote: | Please point to when Go has broken backwards | compatibility. | kaba0 wrote: | That's not my point. | | Smart men learn from the mistakes of others. | nzoschke wrote: | Obviously different perspectives in this thread. | | Robert Griesemer, Rob Pike, and Ken Thompson are | objectively smart men and pioneers and have learned from | lots of mistakes both they and the industry made. | | Go embodied a lot of those learnings out of the gate. | | If the bar is to be perfect out of the gate that's | impossible and I can't think of any language that could | pretend to be so. | | Go was very good out of the gate and has slowly but | surely evolved to be great. | vlunkr wrote: | So how do you think they are "patching things up" more | than other languages? | sidewndr46 wrote: | This nonsense again? Where it is controlled per module, | effectively making it impossible to review code for correctness | without checking those controls. This is an anti-feature. If you | can't be bothered to make a local copy or reference the correct | variable, the problem is the developer. Not the language. | RadiozRadioz wrote: | The problem is the language when its ergonomics coerce most | developers to assume a construct works in a way it does not. | | Programmers are often at fault when a language complex, but I'd | give them a pass when it's simply counterintuitive. | ravivooda wrote: | Similar: https://github.com/ravivooda/gofor | chen_dev wrote: | the 'fix' to the example in the README should be obvious, but | for reference: | | - for _, e := range es { | | - pumpUp(&e) | | } | | + for i := range es { | | + pumpUp(&es[i]) | | } | timrobinson333 wrote: | I'll be the first to admit I know almost nothing about go, but | it's surprises me to find we're still inventing languages with | bobby traps like this, especially bobby traps that were well | known and understood in other languages at the time. | | Actually it surprises me we're still inventing languages where | local variables can be mutated, which seems to be at the root of | the problem here | wrboyce wrote: | Completely unrelated to the point you're making, but the phrase | is "booby trap"; I believe it originates from pranks played on | younger schoolboys in 1600s England (the etymology of booby | being the Spanish "bobo"). | mixmastamyk wrote: | > where local variables can be mutated | | They aren't local, but belong to the outer scope. The | misconception in a nutshell. | tazjin wrote: | Go has a long list of booby traps like this and prides itself | on them. From outside of the Go team it looks like a small | cultural shift might slowly be happening, cleaning up some of | the obvious mistakes everyone's been telling them about since | the beginning. Rob Pike retiring and giving up some formal | power with that probably helps. | thiht wrote: | > Go has a long list of booby traps like this | | Huh? Where's the list? From the top of my head I think this | is the only thing that repeatedly bit me, although I'm very | aware of the behavior of for loop scoping. Linters save me | nowadays at least. | | Are there other things like that in the language that deserve | a fix? Maybe things to do with json un/marshaling? | the_gipsy wrote: | My number one is not having algebraic/sum/union types | leading to needing zero-values. Which is more like a never | idling foot gatling gun. | [deleted] | wazzaps wrote: | Copying a mutex by value (thus duplicating the lock, | causing deadlocks or worse) is far too easy | icholy wrote: | `go vet` catches this. | erik_seaberg wrote: | Is there a way to declare any type uncopyable? This is | something I always thought Ada got right. | icholy wrote: | There isn't. | smasher164 wrote: | you stick this in your struct type | noCopy struct{} func (*noCopy) Lock() {} | func (*noCopy) Unlock() {} | mhh__ wrote: | I use a tool called "type theory" | usefulcat wrote: | Wow, even c++ won't let you do that (without invoking | undefined behavior, anyway). | rsc wrote: | Speaking as the person Rob Pike handed the formal power to (8 | years ago now), I don't think that change has much to do with | it. | | We've known about the problem for a long time. I have notes | from the run up to Go 1 (circa 2011) where we considered | making this change, but it didn't seem like a huge problem, | and we were concerned about breaking old code, so on balance | it didn't seem worth it. | | Two things moved the needle on this particular change: | | 1. A few years ago David Chase took the time to make the | change in the compiler and inventory what it broke in a large | code base (Google's, but any code base would have worked for | that purpose). Seeing that real-world data made it clear that | the problem was more serious than we realized and needed to | be addressed. That is, it made clear that the positive side | of the balance was heavier than we thought it was back in | 2011. | | 2. The design of Go modules added a go version line, which we | can key the change off. That completely avoids breaking any | old code. That zeroed out the negative side of the balance. | orblivion wrote: | What about let's say 5 years from now, someone digs up a Go | project from 2022, decides to get it up to speed for 2028, | updates the version line. Is there something that would | remind them to check for breaking changes, including this | one? Perhaps the go project initializer could add a comment | above the version line with a URL with a list of such | changes. Though, that wouldn't help for this change. | [deleted] | flakes wrote: | I think the key difference here is to consider toleration | vs adoption. Old code is able to tolerate the changes and | still work in new ecosystems. There is still work on | maintainers if they want to actually adopt the features | themselves. Allowing these two concepts to work together | is what allows iteratively updating the world, rather | than requiring big bang introduction of features. | | As for validating your software, the answer is the same | as its always been... tests, tests and more tests. | smasher164 wrote: | > we're still inventing languages where local variables can be | mutated | | Local mutability is probably one of the most common uses of | mutability. A lot of it is using local state to build up a more | complicated structure, and then getting rid of that state. | Getting rid of that use-case is just giving up performance. | [deleted] | zdimension wrote: | There is a recurring joke about Go's language design ignoring | many bits of the general language design knowledge collectively | acquired through decades of writing new languages. This change | is an example of why this joke exists. | kzrdude wrote: | I have run into this problem in Python too, but not recently. I'm | not sure if Python has changed or if I just caught on to the | problem. | | This should be enough to show that it still can wind up as a | problem in Python: funcs = [(lambda: x) for x | in range(3)] funcs[0]() # outputs: 2 | pulvinar wrote: | GPT-4 says: The behavior you're observing is due to the late | binding nature of closures in Python. When you use a lambda | inside a list comprehension (or any loop), it captures a | reference to the variable x, not its current value. By the time | you call funcs[0](), x has already been set to the last value | in the range, which is 2. | | To get the desired behavior, you can pass x as a default | argument to the lambda: funcs = [(lambda x=x: | x) for x in range(3)] funcs[0]() # outputs 0 | paulddraper wrote: | That is correct. | | Python used to be worse; it used to share scope _outside the | list comprehension_. | DonnyV wrote: | GO syntax is so hard to look at. | knodi wrote: | If you hate readability, sure. | guessmyname wrote: | > _GO syntax is so hard to look at._ | | What do you mean by "hard"? | | I find Rust syntax challenging to grasp in a specific way. Rust | employs numerous symbols and expressions to convey statements, | which makes reading Rust code a process of constantly | navigating between different keywords, left and right. I have | to create a mental map of what certain statements are | accomplishing before I can truly comprehend the code. | | In contrast, I find Go code relatively straightforward, | especially for those familiar with C-like programming | languages. This clarity is due to the deliberate verbosity of | the language, which I personally appreciate, as well as the use | of early return statements. | | But don't get me wrong. I enjoy programming in both Rust and Go | when they are suitable for the task at hand, but I usually | spend more time grappling with Rust's syntax than with Go's, | because I often invest more time in understanding the structure | and logic of Rust programs compared to their Go counterparts. | tuetuopay wrote: | I guess it depends on the way the brain works. I have very | bad memory, but I prefer the expressiveness of Rust to the | verbosity of Go. I value much more having the whole context | on the screen that navigating countless words of boilerplate | code. I do agree that it gets a bit of getting used to, but I | find it easier to recognize by eye. | ShamelessC wrote: | At least part of the issue for me was that many | keywords/syntax rules don't match anything I'm familiar with, | even considering "C-like" languages. | | I have similar issues with Rust actually. There's a lot of | sugar used that you have to grok and that takes some time. | | On the other hand Python, C#, Java all stick with a set of | fairly familiar conventions. In terms of syntax (and only | syntax), the learning curve is more intense with Go; perhaps | similar to the initial alienation provided by JavaScript. | | My experience has been that once you are being paid to learn | a language these problems mostly disappear. Alas, no one ever | paid me to learn Go. | campbel wrote: | Won't this end up breaking programs that depend on the current | behavior? | [deleted] | minroot wrote: | Some time spent with Go gives a strong indication that Go team | always has backwards compatibility in mind | jacquesm wrote: | They do. Go has avoided most of the pitfalls that other | language eco-systems have fallen for over the years | (backwards compatibility issues, soft forks masquerading as | language improvements, re-booting the whole language under | the same name, aggressively pushing down on other languages | etc). They've done _remarkably_ well in those respects, and | should deserve huge credit for it. | nerdponx wrote: | Python has the same problem (to the extent that it's actually a | problem, which you might or might not agree with), and this is | the #1 reason they won't change it. | omeid2 wrote: | I don't know why you're being down voted, but it is actually | breaking the Go1 compat promise. Which says: | It is intended that programs written to the Go 1 specification | will continue to compile and run correctly, unchanged, over the | lifetime of that specification. At some indefinite point, a Go | 2 specification may arise, but until that time, Go programs | that work today should continue to work even as future "point" | releases of Go 1 arise (Go 1.1, Go 1.2, etc.). | jacquesm wrote: | And they do. You can specify the precise logic to use on a | per-file basis. | wrs wrote: | Note the word "programs", not "files". If your program | doesn't declare go 1.22 in its go.mod, it will continue to | work (or not work!), unchanged. | robertlagrant wrote: | Isn't that "working with future point releases", though? If | I don't declare 1.22, am I not excluded from that point | release? | ben0x539 wrote: | I assume that if compiling with 1.22 or later, you still | get all the benefits from that version like other new | features, bug fixes or perf improvements, just not this | particular change. | mrkstu wrote: | No, the compiler will revert to the original behavior, it | only adopts the new behavior with the declaration. | freedomben wrote: | I upvoted the question to offset one of the downs because I | agree it's a fair question. However I would guess the | downvotes are because TFA addressed this issue directly and | comprehensively, so it's a clear "I didn't read the article" | indicator :-) Possibly also because the downvoters can't | imagine a scenario where this would be desirable behavior | (i.e. it's always a bug) | colejohnson66 wrote: | But if it's a bug, then the logic to not compile future | versions is wrong, IMO. If it's a feature change, then such | logic would make sense. | campbel wrote: | Yeah its fair, I didn't closely read that section. | Although, I'm not entirely convinced the approach is safe, | maybe its worth it to fix such a common pitfall. | [deleted] | campbel wrote: | I don't mind. | | Yeah, I thought this kind of change wouldn't happen because | of this promise. | tgv wrote: | I think it was downvoted precisely because of that. It's a | bit of a contentious issue. | doctor_eval wrote: | To add to the other comments, in the run-up to go1.21 they | talked about how they'd analysed a very large corpus of Go code | to see what would be affected, and it was a very very small | number. | | I remember thinking that the number of people who have created | inadvertent bugs due to this design (myself included) would be | significantly greater than the number of people affected by the | fix. | omginternets wrote: | Seems like yes, though hopefully that should be rare. | matthewmueller wrote: | It's only enabled for modules that run Go 1.22 and higher | badrequest wrote: | I also can't imagine a case where it is useful or even truly | intended to rely on this behavior. | ben0x539 wrote: | Yeah I don't think it's so much "we explicitly rely on this | behavior, how dare you change this" as "somewhere in our | mountains of maintenance-mode code that haven't seen the | sun shine through an editor window in years, this behavior | cancels out another bug that we never noticed". Tooling | should be able to detect when code relies on this, but it's | still gonna cost some non-zero amount of developer effort | to touch ancient code and safely roll out a new version if | it needs to be actively addressed. | rsc wrote: | If you have tests and they break with | GOEXPERIMENT=loopvar, then there is a new tool that will | tell you exactly which loop is causing the breakage. | That's a post for a few weeks from now. | omginternets wrote: | Yeah it's def a code smell ... | infogulch wrote: | > To ensure backwards compatibility with existing code, the new | semantics will only apply in packages contained in modules that | declare go 1.22 or later in their go.mod files. ... It is also | possible to use //go:build lines to control the decision on a | per-file basis. | sixstringtheory wrote: | Doesn't that mean that all code written so far can't take up | newer versions of the Go compiler for any other reason like | new features/bugfixes/optimizations/etc without a full audit | of codepaths involving for loops? | ericpauley wrote: | No, it does not. Packages can compile using 1.22 and gain | other benefits without opting into this change. | sixstringtheory wrote: | Ah, I didn't see the part about //go:build | matthewmueller wrote: | Anyone know how this will impact loop performance? | tedunangst wrote: | Nonexistent? It can still reuse the memory if you don't capture | it. And if you were capturing it "properly" it was already | making a copy. | jsmith45 wrote: | Most commonly no impact. It can require an additional heap | allocation per iteration if taking the address or capturing in | a closure, but even in those cases escape analysis may be able | to determine that the value can remain on the stack because it | will not remain referenced longer than the current loop | iteration. If that happens then this change has no impact. | | I'm not sure how thorough Go's escape analysis is, but nearly | all programs that capture the loop variable in a closure and | are not buggy right now could be shown to have that closure not | escape by a sufficient thorough escape analysis. On the other | hand for existing buggy programs, then perf hit is the same as | assigning a variable and capturing that (the normal fix for the | bug). | | Google saw no statistically significant change in their | benchmarks or internal applications. | | https://github.com/golang/go/wiki/LoopvarExperiment#will-the... | mongol wrote: | Is this a common way to fix problems in language syntax? It seems | unintuitive to me. Now you need to know what version is declared | in one file to understand behavior in another file. I understand | they want to fix this but I did not know this way was allowed. | jjwiseman wrote: | I know there are much earlier examples, but the earliest warning | about this behavior I could find in 60 seconds of searching is | from the comp.lang.lisp FAQ, posted more than 30 years ago, in | 1992: Mar 21, 1992, 1:00:47 AM Last- | Modified: Tue Feb 25 17:34:30 1992 by Mark Kantrowitz ;;; | **************************************************************** | ;;; Answers to Frequently Asked Questions about Lisp | *************** ;;; | **************************************************************** | ;;; Written by Mark Kantrowitz and Barry Margolin ;;; | lisp-faq-3.text -- 16886 bytes [...] | ---------------------------------------------------------------- | [3-9] Closures don't seem to work properly when referring to the | iteration variable in DOLIST, DOTIMES and DO. | DOTIMES, DOLIST, and DO all use assignment instead of binding to | update the value of the iteration variables. So something like | (dotimes (n 10) (push #'(lambda () (incf n)) | *counters*)) will produce 10 closures over the | same value of the variable N. | ---------------------------------------------------------------- ___________________________________________________________________ (page generated 2023-09-19 23:00 UTC)