From 91d4279670242636ad88ed7293a0a45b89240c83 Mon Sep 17 00:00:00 2001 From: Go101 <22589241+go101@users.noreply.github.com> Date: Fri, 7 Jun 2024 21:34:30 +0800 Subject: [PATCH 1/3] small changes --- ...-for-loop-semantic-changes-in-go-1.22.html | 248 +++++------------- ...1-for-loop-semantic-changes-in-go-1.22.tmd | 240 +++++++++-------- pages/fundamentals/evaluation-orders.html | 4 +- .../keywords-and-identifiers.html | 6 +- web/static/go101/css/v995-dark.css | 176 +++++++++++++ web/static/go101/css/v995-light.css | 166 ++++++++++++ web/static/go101/js/v8.js | 103 ++++++++ web/templates/article | 6 +- 8 files changed, 638 insertions(+), 311 deletions(-) create mode 100644 web/static/go101/css/v995-dark.css create mode 100644 web/static/go101/css/v995-light.css create mode 100644 web/static/go101/js/v8.js diff --git a/pages/blog/2024-03-01-for-loop-semantic-changes-in-go-1.22.html b/pages/blog/2024-03-01-for-loop-semantic-changes-in-go-1.22.html index eac851f3..8d97bd7c 100644 --- a/pages/blog/2024-03-01-for-loop-semantic-changes-in-go-1.22.html +++ b/pages/blog/2024-03-01-for-loop-semantic-changes-in-go-1.22.html @@ -1,137 +1,3 @@ - - - - - -

for Loop Semantic Changes in Go 1.22: Be Aware of the Impact

@@ -181,13 +47,15 @@

What are the changes?

We can install multiple Go toolchain versions to check the outputs. Here, I use the GoTV tool to (conveniently) choose Go toolchain versions.

-The outputs:
$ gotv 1.21. run demo1.go
+
+
+The outputs:
$ gotv 1.21. run demo1.go
 [Run]: $HOME/.cache/gotv/tag_go1.21.7/bin/go run demo1.go
 14
 $ gotv 1.22. run demo1.go
 [Run]: $HOME/.cache/gotv/tag_go1.22.0/bin/go run demo1.go
 10
-
+

The behavior difference is obvious: @@ -253,13 +121,13 @@

What are the changes?

-The outputs of the above program:
$ gotv 1.21. run demo2.go
+The outputs of the above program:
$ gotv 1.21. run demo2.go
 [Run]: $HOME/.cache/gotv/tag_go1.21.7/bin/go run demo2.go
 12
 $ gotv 1.22. run demo2.go
 [Run]: $HOME/.cache/gotv/tag_go1.22.0/bin/go run demo2.go 
 6
-
+

This article focuses on the details of the changes and impact of the changes, rather than the reasons behind them. For details on the approval process and reasons behind the changes, see @@ -289,6 +157,8 @@

What are the changes?

+
+

The impact of the changes

@@ -310,6 +180,8 @@

The impact of the changes

+ +
By the speficication, since Go 1.22, the loop shown above is actually equivalent to the following pseudo-code (Sorry, the new semantics are hard to explain in a clear and perfect way. None of Go official documentations ever successfully achieve this goal. Here, I have tried my best.):

{
@@ -335,7 +207,7 @@ 

The impact of the changes

Wow, quite a lot of magical implicit code. For a language that promotes explicitness, it's embarrassing.

-Implicitness often leads to unexpected surprises, which is not a surprise. The following will show several examples which might break your expectations. +Implicitness often leads to unexpected surprises, which is not a surprise. The following will show several cases which might break your expectations.

The behaviors of deferred function calls which capture loop variables might change

@@ -359,7 +231,7 @@

The behaviors of deferred function calls which capture loop variables might

-Its outputs:
$ gotv 1.21. run demo-defer.go
+Its outputs:
$ gotv 1.21. run demo-defer.go
 [Run]: $HOME/.cache/gotv/tag_go1.21.7/bin/go run demo-defer.go
 #0: 0
 #1: 1
@@ -369,7 +241,7 @@ 

The behaviors of deferred function calls which capture loop variables might #0: 0 #0: 1 #0: 2 -

+

You can find that, since Go 1.22, the value of counter is never effectively increased. Why? I'm sorry. As mentioned above, it is some hard to clearly explain the new semantics and I don't think I have the ability to do this. You may get it from the following equivalent code: @@ -385,7 +257,7 @@

The behaviors of deferred function calls which capture loop variables might } else { n-- } - + if !(n >= 0) { break } @@ -422,7 +294,7 @@

The behaviors of deferred function calls which capture loop variables might }(i) } } - + r = make([]int, count) // only allocate once return } @@ -433,13 +305,13 @@

The behaviors of deferred function calls which capture loop variables might

-The outputs of the above program:
$ gotv 1.21. run search.go
+The outputs of the above program:
$ gotv 1.21. run search.go
 [Run]: $HOME/.cache/gotv/tag_go1.21.7/bin/go run search.go
 [8 6 4 2 0]
 $ gotv 1.22. run search.go
 [Run]: $HOME/.cache/gotv/tag_go1.22.0/bin/go run search.go
 [0 0 0 0 0]
-
+

So, since Go 1.22, just be careful when using freshly-declared loop variables in deferred function calls. @@ -454,6 +326,8 @@

The behaviors of deferred function calls which capture loop variables might

+ +
However, sadly, the suggestion was ignored totally. @@ -485,13 +359,13 @@

Be careful when capturing loop variables in closures

-Its outputs:
$ gotv 1.21. run demo-closure-1.go
+Its outputs:
$ gotv 1.21. run demo-closure-1.go
 [Run]: $HOME/.cache/gotv/tag_go1.21.7/bin/go run demo-closure-1.go
 9
 $ gotv 1.22. run demo-closure-1.go
 [Run]: $HOME/.cache/gotv/tag_go1.22.0/bin/go run demo-closure-1.go
 0
-
+

Prior to Go 1.22, what the printN closure captures is the only instance of the loop variable, which final value is 9. However, since Go 1.22, what the printN closure captures is the first instance of the loop variable, which final value is 1. That is the reason of the behavior difference between the two Go versions. @@ -521,13 +395,13 @@

Be careful when capturing loop variables in closures

-Its outputs:
$ gotv 1.21. run demo-closure-2.go
+Its outputs:
$ gotv 1.21. run demo-closure-2.go
 [Run]: $HOME/.cache/gotv/tag_go1.21.7/bin/go run demo-closure-2.go
 abcdefghijklmnopqrstuvwxyz
 $ gotv 1.22. run demo-closure-2.go
 [Run]: $HOME/.cache/gotv/tag_go1.22.0/bin/go run demo-closure-2.go
 a
-
+

The third example: @@ -554,9 +428,7 @@

Be careful when capturing loop variables in closures

- -

Be careful when taking addresses of loop variables

- +#:::::::::::::::::::::::::::::::::::::::::::::::::; Be careful when taking addresses of loop variables

Similarly, since Go 1.22, it may be dangerous to use the address of a freshly-declared loop variable across loop iterations.

@@ -575,13 +447,13 @@

Be careful when taking addresses of loop variables

-Its outputs:
$ gotv 1.21. run demo-pointer1.go
+Its outputs:
$ gotv 1.21. run demo-pointer1.go
 [Run]: $HOME/.cache/gotv/tag_go1.21.7/bin/go run demo-pointer1.go
 true
 $ gotv 1.22. run demo-pointer1.go
 [Run]: $HOME/.cache/gotv/tag_go1.22.0/bin/go run demo-pointer1.go
 false
-
+

Go 1.21 and 1.22 give different answers. Why? From the equivalent code shown below, we can get that, in the comparison p == &i, p points to the first instance of i, whereas &i takes the address of the second instance of i. So the comparison evaluation result is false. @@ -624,7 +496,7 @@

Be careful when taking addresses of loop variables

-Since Go 1.22, the above program will never exit (prior to Go 1.22, it will):
$ gotv 1.21. run demo-pointer2.go
+Since Go 1.22, the above program will never exit (prior to Go 1.22, it will):
$ gotv 1.21. run demo-pointer2.go
 [Run]: $HOME/.cache/gotv/tag_go1.21.7/bin/go run demo-pointer2.go
 0
 1
@@ -635,12 +507,10 @@ 

Be careful when taking addresses of loop variables

0 0 ... -
+

- -

Be careful when moving the 3rd clause statements inside loop bodies

- +#::::::::::::::::::::::::::::::::::::::::::::::::; Be careful when moving the 3rd clause statements inside loop bodies

Since Go 1.22, the following two loops might be not equivalent with each other any more (prior to Go 1.22, they are equivalent).

@@ -686,18 +556,16 @@

Be careful when moving the 3rd clause statements inside loop bodies

-The new outputs:
$ gotv 1.22. run demo-pointer3.go
+The new outputs:
$ gotv 1.22. run demo-pointer3.go
 [Run]: $HOME/.cache/gotv/tag_go1.22.0/bin/go run demo-pointer3.go
 true
 0
 1
 2
-
+

- -

Be careful when declaring no-copy values as loop variables

- +#::::::::::::::::::::::::::::::::::::::::::::::::::; Be careful when declaring no-copy values as loop variables

As explained above, since Go 1.22, at the start of each loop iteration, each freshly-declared loop variable will get copied once, implicitly. The implication means that, since Go 1.22, it is not a good idea to declare no-copy values as loop variables, such as sync.Mutex, sync/atomic.Int64, bytes.Buffer, and strings.Builder values etc.

@@ -716,7 +584,7 @@

Be careful when declaring no-copy values as loop variables

if (wait == nil) { wait = wg.Wait } - + wg.Add(1) go func(v int) { defer wg.Done() @@ -735,7 +603,7 @@

Be careful when declaring no-copy values as loop variables

-Its outputs:
$ gotv 1.21. run demo-nocopy1.go
+Its outputs:
$ gotv 1.21. run demo-nocopy1.go
 [Run]: $HOME/.cache/gotv/tag_go1.21.8/bin/go run demo-nocopy1.go
 0
 2
@@ -745,7 +613,7 @@ 

Be careful when declaring no-copy values as loop variables

0 $ gotv 1.22. vet demo-nocopy1.go [Run]: $HOME/.cache/gotv/tag_go1.22.1/bin/go vet demo-nocopy1.go -
+

Note that the go vet command in Go 1.22 toolchain can't catch such implicit duplication of no-copy values, regardless of whether the loop variable wg is captured in the loop body or not. @@ -781,7 +649,7 @@

Be careful when declaring no-copy values as loop variables

-Run it with different Go toolchains, we get:
$ gotv 1.21. run demo-nocopy2.go
+Run it with different Go toolchains, we get:
$ gotv 1.21. run demo-nocopy2.go
 [Run]: $HOME/.cache/gotv/tag_go1.21.7/bin/go run demo-nocopy2.go
 abcdefghijklmnopqrstuvwxyz
 $ gotv 1.22. run demo-nocopy2.go
@@ -790,7 +658,7 @@ 

Be careful when declaring no-copy values as loop variables

goroutine 1 [running]: ... -
+

Yes, the run-time no-copy check works. Since Go 1.22, when the loop variable of type strings.Builder gets duplicated, a panic is created. Prior to Go 1.22, this duplication will not happen, so there will be no panic. @@ -799,20 +667,22 @@

Be careful when declaring no-copy values as loop variables

Let's change the Debug constant in the above example to false, then run the example again with the 1.22 toolchain.

-
$ gotv 1.22. run demo-nocopy.go
+
$ gotv 1.22. run demo-nocopy.go
 [Run]: $HOME/.cache/gotv/tag_go1.22.0/bin/go run demo-nocopy.go
 abcdefghijklmnopqrstuvwxyz
-
+

We can find that no panic occurs now. Why? Because now the if (Debug) { callback(&b) } line becomes into dead code so that the compiler thinks that each instance of the loop variable b is used solely within the corresponding iteration's lifetime. So the loop variable b is instantiated only once for the entire loop and no duplication happens for the only instance.

However, the compiler is too smart to make a bad decision here. The compiler incorrectly implements the semantics. The example program should panic regardless of the value of the Debug constant. While this specific case might be considered acceptable due to the lack of harmful consequences, it raises concerns about the potential for unexpected behavior in other scenarios.

+ +
The safe advice is try not to declare no-copy values as loop variables. This is just a suggestion, not a mandatory rule, because copying no-copy values does not always cause damage (but the damage may be exposed later when the code is refactored in some way).

-

Warning: the performance of your Go programs might be degraded silently

+

Warning: the performance of your Go programs might be degraded silently

Sometimes, a compiler is over smart; sometimes, it is not smart enough. For example, sometimes, the official standard compiler provided in Go toolchain 1.22 is unable to determine that each instance of a freshly-declared loop variable is used solely within the corresponding iteration's lifetime, so that the loop variable will be instantiated per iteration and each of its instances will be allocated on heap instead of stack. Even worse, if the size of the loop variable is large, then high duplication costs will be incurred. When these situations occur, the performance of the program will be degraded. @@ -855,7 +725,7 @@

Warning: the performance of your Go programs might be degraded silently

-Its outputs:
$ gotv 1.21. run aaa.go
+Its outputs:
$ gotv 1.21. run aaa.go
 [Run]: $HOME/.cache/gotv/tag_go1.21.7/bin/go run aaa.go
 foo time: 3.573µs
 bar time: 3.267µs
@@ -863,7 +733,7 @@ 

Warning: the performance of your Go programs might be degraded silently

[Run]: $HOME/.cache/gotv/tag_go1.22.0/bin/go run aaa.go foo time: 3.819µs bar time: 552.246µs -
+

The benchmark results reveal a significant performance regression in the bar function between Go 1.21 and 1.22. Why? Because, with the official standard Go compiler 1.22, the loop variable a in the bar function is duplicated in each iteration. Whereas in prior versions, such duplication is always needless. @@ -912,7 +782,7 @@

Warning: things might become more subtle than before when loop variables are

-The above program is intended to print the values of the loop variable i at each iteration. Prior to Go 1.22, there is a clear data race condition present in the program, because the loop variable i is only instantiated once during the whole loop. All the new created goroutines will read the single instance but the main goroutine will modify it. The following outputs prove this fact:
$ CGO_ENABLED=1 gotv 1.21. run -race demo-concurency1.go
+The above program is intended to print the values of the loop variable i at each iteration. Prior to Go 1.22, there is a clear data race condition present in the program, because the loop variable i is only instantiated once during the whole loop. All the new created goroutines will read the single instance but the main goroutine will modify it. The following outputs prove this fact:
$ CGO_ENABLED=1 gotv 1.21. run -race demo-concurency1.go
 [Run]: $HOME/.cache/gotv/tag_go1.21.7/bin/go run -race demo-concurency1.go
 3
 3
@@ -921,15 +791,15 @@ 

Warning: things might become more subtle than before when loop variables are ... ================== 3 -

+

-Prior to Go 1.22, the fix is simple, just add an i := i line at the start of the loop body. Go 1.22 fixes the specified data race problem by changing the semantics of for;; loops, without modifying the old problematic code. This can be verified by the following outputs:
$ CGO_ENABLED=1 gotv 1.22. run -race demo-concurency1.go
+Prior to Go 1.22, the fix is simple, just add an i := i line at the start of the loop body. Go 1.22 fixes the specified data race problem by changing the semantics of for;; loops, without modifying the old problematic code. This can be verified by the following outputs:
$ CGO_ENABLED=1 gotv 1.22. run -race demo-concurency1.go
 [Run]: $HOME/.cache/gotv/tag_go1.22.0/bin/go run -race demo-concurency1.go
 1
 2
 0
-
+

In fact, this is just the reason why Go 1.22 made the semantic change to for;; loops. But is it worth it to fix such a small problem by introducing magical implicit code? @@ -961,7 +831,7 @@

Warning: things might become more subtle than before when loop variables are

Is the new code still data race free (with Go 1.22 semantics)? It looks good. Each new created goroutine just uses an exclusive copy of the loop variable i. But the answer is "no", because there is an implicit assignment at the start of each iteration and the implicit assignment uses an instance of the loop variable as source value (a.k.a. the main goroutine reads it), however the instance is modified in a new created goroutine.

-The following outputs verify there is a data race condition present in the new code:
$ CGO_ENABLED=1 gotv 1.22. run -race demo-concurency2.go
+The following outputs verify there is a data race condition present in the new code:
$ CGO_ENABLED=1 gotv 1.22. run -race demo-concurency2.go
 [Run]: $HOME/.cache/gotv/tag_go1.22.0/bin/go run -race demo-concurency2.go
 ==================
 WARNING: DATA RACE
@@ -971,7 +841,7 @@ 

Warning: things might become more subtle than before when loop variables are 1 3 Found 1 data race(s) -

+

Prior to Go 1.22, the data race is clear and easily to detect. But since Go 1.22, things become more subtle and the data race is not very clear (because of the implicit code). @@ -1004,14 +874,14 @@

Warning: things might become more subtle than before when loop variables are i++ v := i m.Unlock() - + if isGold(v) { c <- v } } }() } - + for n := range c { fmt.Println("Found gold", n) } @@ -1019,7 +889,7 @@

Warning: things might become more subtle than before when loop variables are

-Run it with different toolchain versions, get the following outputs:
$ CGO_ENABLED=1 gotv 1.21. run -race demo-concurency3.go
+Run it with different toolchain versions, get the following outputs:
$ CGO_ENABLED=1 gotv 1.21. run -race demo-concurency3.go
 [Run]: $HOME/.cache/gotv/tag_go1.21.7/bin/go run -race demo-concurency3.go
 Found gold 1048576
 Found gold 2097152
@@ -1043,7 +913,7 @@ 

Warning: things might become more subtle than before when loop variables are Found gold 3145728 ... ^C -

+

😳😳😳... (Consider that the title of the proposal to make the semantic change is "Proposal: Less Error-Prone Loop Variable Scoping".) @@ -1082,6 +952,8 @@

Specify Go language versions for Go source files

+ +
If the Go language version of a Go source file is not specified by any of the above ways, then the version of the used Go compiler is used. In other words, the behavior of the code in the source file is compiler dependent.

The design causes two problems: @@ -1098,6 +970,8 @@

Specify Go language versions for Go source files

+ +
Anyway, since Go 1.22, you should try to specify a Go language version for every Go source file, in any of the above introduced ways, to avoid compiler version dependent behaviors. This is the minimum standard to be a professional Go programmer in the Go 1.22+ era.

@@ -1157,7 +1031,7 @@

Final words

In my honest opinion, the benefits of the new semantics of for;; loops are rare and tiny, whereas the drawbacks are more prominent and serious.

-The semantic changes introduced in Go 1.22 significantly lower the threshold for maintaining backward compatibility. This is a bad start. +The semantic changes introduced in Go 1.22 significantly lower the standard for maintaining backward compatibility. This is a bad start.

I have expressed my opinions in the following comments: