> Go 1.25 introduced a waitgroup.Go function that lets you add Go routines to a waitgroup more easily. It takes the place of using the go keyword, [...]
99% of the time, you don't want to use sync.WaitGroup, but rather errgroup.Group. This is basically sync.WaitGroup with error handling. It also has optional context/cancellation support. See https://pkg.go.dev/golang.org/x/sync/errgroup
I know it's not part of the standard library, but it's part of the http://golang.org/x/ packages. TBH, golang.org/x/ is stuff that should be in the standard library but isn't, for some reason.
I thought exactly the same thing. I use errgroup in practically every Go project because it does something you'd most likely do by hand otherwise, and it does it cleaner.
I discovered it after I had already written my own utility to do exactly the same thing, and the code was almost line for line the same, which was pretty funny. But it was a great opportunity to delete some code from the repo without having to refactor anything!
> and the code was almost line for line the same, which was pretty funny.
One of the core strengths of Go is that it fits the zen of Python's " There should be one-- and preferably only one --obvious way to do it" and it does this very nicely.
I never used errgroup but I realize that it's essentially the same what I end up implementing anyways.
With standard waitgroups I always move my states as a struct with something like a nested *data struct and an err property which is then pushed through the channel. But this way, my error handling is after the read instead of right at the Wait() call.
There is mention of how len() is bytes, not “characters”. A further subtlety: a rune (codepoint) is still not necessarily a “character” in terms of what is displayed for users — that would be a “grapheme”.
A grapheme can be multiple codepoints, with modifiers, joiners, etc.
Did not know about index-based string interpolation. Useful!
The part about changing a map while iterating is wrong though. The reason you may or may not get it is because go iterates in intentionally random order. It's nothing to do with speed. It's to prevent users from depending on the iteration order. It randomly chooses starting bucket and then goes in circular order, as well as randomly generates a perm of 0..7 inside each bucket. So if your edit goes into a bucket or a slot already visited then it won't be there.
Also, python is not an example to the contrary. Modifying python dicts while iterating is a `RuntimeError: dictionary changed size during iteration`
Great list of why one can love and hate Go. I really did enjoy writing it but you never get the sense that you can be truly certain your code is robust because of subtle behaviour around nil.
I guess as a corollary, Go really rewards writing the dumbest code possible. No advanced type shenanigans, no overuse of interfaces, no complex composition of types. Then you will end up with a very fast, resource light system that just runs forever.
And code with zero ability to do fancy trickery ("expressive" as some people like to say) is easy to read even if the codebase - or even the language - is unfamiliar.
Which is really handy when shit's on fire and you need to find the error yesterday. You can just follow what happens instead of trying to figure out the cool tricks the original programmer put in with their super-expressive language.
Yes, the bug is on line 42, but it does two dozen things on the single line...
You could say the same thing about any language. Writing "dumb" code is easier to understand especially in the small. But Go with function that take functions and return functions, channels and generics, can quickly look as complex as other languages.
To be fair, checking if an interface is nil is very dumb code, and the fact that it doesn't work is one of my biggest gripes with the language. In this case it's clearly the language (creators) who's dumb
>As an additional complexity, although string literals are UTF-8 encoded, they are just aribtrary collections of bytes, which means you can technically have strings that have invalid data in them. In this case, Go replaces invalid UTF-8 data with replacement characters.
No, it's just doing the usual "replace unprintable characters when printing" behavior. The data is unchanged, you have no guarantees of UTF-8 validity at all: https://go.dev/play/p/IpYjcMqtmP0
> This is different than, for instance, python, which has a “stable insertion order” that guarantees that this won’t happen. The reason Go does this: speed!
In Python you'll actually get a RuntimeError here, because Python detects that you're modifying the dictionary while iterating over it.
I'm amused by posts like this because it shows that Go is finally slowly moving away from being an unergonomically simplistic language (its original USP?) to adopt features a modern language should have had all along.
My experience developing in it always gave me the impression that the designers of the language looked at C and thought "all this is missing is garbage collection and then we'll have the perfect language".
I feel like a large amount of the feeling of productivity developers get from writing Go code originates from their sheer LOC output due to having to reproduce what other languages can do in just a few lines thanks to proper language & standard library features.
Unfortunely given that the authors are also related to C's creation, it shows a common pattern, including why C is an insecure language.
> Although we entertained occasional thoughts about implementing one of the major languages of the time like Fortran, PL/I, or Algol 68, such a project seemed hopelessly large for our resources: much simpler and smaller tools were called for. All these languages influenced our work, but it was more fun to do things on our own.
> Rob Pike later explained Alef's demise by pointing to its lack of automatic memory management, despite Pike's and other people's urging Winterbottom to add garbage collection to the language;
I remember when I first got out of uni and did backend Java development, I thought I was incredibly productive because of the sheer amount of typing and code I had to pump out.
After doing a bit of frontend JS I was quickly dissuaded of that notion, all I was doing was writing really long boilerplate.
This was in the Java 6 days, so before a lot of nice features were added, for example a simple callback required the creation of a class that implements an interface with the method (so 3 unique names and a bunch of boilerplate to type out, you could get away with 2 names if you used an anonymous class).
As a Go developer, I do think that I end up writing more code initially, not just because of the lack of syntactic sugar and "language magic", but because the community philosophy is to prefer a little bit of copying over premature abstraction.
I think the end result is code which is quite easy to understand and maintain, because it is quite plain stuff with a clear control flow at the end of the day. Go code is the most pleasant code to debug of all the languages I've worked with, and there is not a close second.
Given that I spend much more time in the maintenance phase, it's a trade-off I'm quite happy to make.
C at least has const ptr. In go I've seen pointers mutated 7 levels down the callstack. And of course, the rest of the sphagetti depended on those side effects.
C is so limited that you would try to avoid mutation and even complex datastructures.
Go is "powerful" enough to let you shoot yourself much harder.
Go with `const` and NonNull<ptr> (call it a reference if you need) would be a much nicer language
If you don't write Go at all, this blog post isn't going to be useful to you, and you aren't its audience. It's fine not to have an apt take for a programming-language-specific article!
The wording "Subtleties" used here is some weird/improper. I see nothing subtle here. They are all basic knowledge a qualified Go programmer should know about.
> Go 1.25 introduced a waitgroup.Go function that lets you add Go routines to a waitgroup more easily. It takes the place of using the go keyword, [...]
99% of the time, you don't want to use sync.WaitGroup, but rather errgroup.Group. This is basically sync.WaitGroup with error handling. It also has optional context/cancellation support. See https://pkg.go.dev/golang.org/x/sync/errgroup
I know it's not part of the standard library, but it's part of the http://golang.org/x/ packages. TBH, golang.org/x/ is stuff that should be in the standard library but isn't, for some reason.
I discovered it after I had already written my own utility to do exactly the same thing, and the code was almost line for line the same, which was pretty funny. But it was a great opportunity to delete some code from the repo without having to refactor anything!
One of the core strengths of Go is that it fits the zen of Python's " There should be one-- and preferably only one --obvious way to do it" and it does this very nicely.
think of it as testing/staging before being merged into stable stdlib
How does it cancel in-progress goroutines when the provided context is cancelled?
With standard waitgroups I always move my states as a struct with something like a nested *data struct and an err property which is then pushed through the channel. But this way, my error handling is after the read instead of right at the Wait() call.
A grapheme can be multiple codepoints, with modifiers, joiners, etc.
This is true in all languages, it’s a Unicode thing, not a Go thing. Shameless plug, here is a grapheme tokenizer for Go: https://github.com/clipperhouse/uax29/tree/master/graphemes
I'm saving this one. Not exactly how I'd explain it, but it's simplified enough to share with my current co-workers without being misleading.
I do not use Go but ran into this when I had to write a Go wrapper for some Rust stuff the other day. I was baffled.
The part about changing a map while iterating is wrong though. The reason you may or may not get it is because go iterates in intentionally random order. It's nothing to do with speed. It's to prevent users from depending on the iteration order. It randomly chooses starting bucket and then goes in circular order, as well as randomly generates a perm of 0..7 inside each bucket. So if your edit goes into a bucket or a slot already visited then it won't be there.
Also, python is not an example to the contrary. Modifying python dicts while iterating is a `RuntimeError: dictionary changed size during iteration`
P1: The type and its method vtable
P2: The value
Once I understood that I could intuit how a nil Foo was not a nil Bar and not an untyped nil either
willem-dafoe-head-tap.gif
Which is really handy when shit's on fire and you need to find the error yesterday. You can just follow what happens instead of trying to figure out the cool tricks the original programmer put in with their super-expressive language.
Yes, the bug is on line 42, but it does two dozen things on the single line...
Simplicity is hard. You may see it as dumb, other see it as priceless attribute of the language.
Deleted Comment
No, it's just doing the usual "replace unprintable characters when printing" behavior. The data is unchanged, you have no guarantees of UTF-8 validity at all: https://go.dev/play/p/IpYjcMqtmP0
In Python you'll actually get a RuntimeError here, because Python detects that you're modifying the dictionary while iterating over it.
My experience developing in it always gave me the impression that the designers of the language looked at C and thought "all this is missing is garbage collection and then we'll have the perfect language".
I feel like a large amount of the feeling of productivity developers get from writing Go code originates from their sheer LOC output due to having to reproduce what other languages can do in just a few lines thanks to proper language & standard library features.
> Although we entertained occasional thoughts about implementing one of the major languages of the time like Fortran, PL/I, or Algol 68, such a project seemed hopelessly large for our resources: much simpler and smaller tools were called for. All these languages influenced our work, but it was more fun to do things on our own.
From https://www.nokia.com/bell-labs/about/dennis-m-ritchie/chist...
Go grew up from the failed design with Alef in Plan 9, which got a second chance with Limbo on Inferno.
https://en.wikipedia.org/wiki/Alef_(programming_language)
> Rob Pike later explained Alef's demise by pointing to its lack of automatic memory management, despite Pike's and other people's urging Winterbottom to add garbage collection to the language;
https://doc.cat-v.org/inferno/4th_edition/limbo_language/lim...
You will notice some of the similarities between Limbo and Go, with a little sprikle of Oberon-2 method syntax, and SYSTEM replaced by unsafe.
https://ssw.jku.at/Research/Papers/Oberon2.pdf
After doing a bit of frontend JS I was quickly dissuaded of that notion, all I was doing was writing really long boilerplate.
This was in the Java 6 days, so before a lot of nice features were added, for example a simple callback required the creation of a class that implements an interface with the method (so 3 unique names and a bunch of boilerplate to type out, you could get away with 2 names if you used an anonymous class).
I think the end result is code which is quite easy to understand and maintain, because it is quite plain stuff with a clear control flow at the end of the day. Go code is the most pleasant code to debug of all the languages I've worked with, and there is not a close second.
Given that I spend much more time in the maintenance phase, it's a trade-off I'm quite happy to make.
(This is of course all my experience; very IMO)
Deleted Comment
C is so limited that you would try to avoid mutation and even complex datastructures.
Go is "powerful" enough to let you shoot yourself much harder.
Go with `const` and NonNull<ptr> (call it a reference if you need) would be a much nicer language
They are many real subtleties in Go, which even many professional Go programmers are not aware of. Here are some of them: https://go101.org/blog/2025-10-22-some-real-go-subtleties.ht...
“for true {...} and for {...} are not eqivalent”
So what? The compiler will tell you the first time you try to run that “for true” abomination that it is invalid code.
> > “for true {...} and for {...} are not eqivalent”
> So what? The compiler will tell you the first time you try to run that “for true” abomination that it is invalid code.
It teaches you know that, when you write
You can write it as The compiler will not teach you this. ;DUsefulness might be subjective. Personally, the last two subtleties mentioned in the article are useful for me too.
You may find some useful (in your opinion) subtleties in the Go Details and Tips 101 book: https://go101.org/details-and-tips/101.html, and some since-Go-1.22/3 ones here: https://go101.org/blog/2024-03-01-for-loop-semantic-changes-... and https://go101.org/blog/2025-03-15-some-facts-about-iterators...