> 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.
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.
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)
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
func bar() int {
for true {
...
}
return 0 // whatever
}
You can write it as func bar() int {
for {
...
}
}
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...
Is index-based string interpolation easier to follow? I would find it easier to understand a string interpolation when the variable name is right there, rather than having to count along the arguments to find the particular one it's referencing
I also found this very confusing:
> When updating a map inside of a loop there’s no guarantee that the update will be made during that iteration. The only guarantee is that by the time the loop finishes, the map contains your updates.
That's totally wrong, right? It makes it sound magical. There's a light explainer but I think it would be a lot more clear to say that of course the update is made immediately, but the "range" iterator may not see it.
It's all just spelling. Your compiler just turns
x = "I want ${number/12|round} dozen eggs, ${name|namecase}"
into x = StrCon("I want ", round(number/12), " dozen eggs, ", namecase(name))
anyhow. It's not a huge transform.I think people get bizarrely hung up on the tiny details of this between languages... but then, I think that extensive use of string interpolation is generally a code smell at best anyhow, so I'm probably off the beaten path in more than one way here.
To write? Maybe so. Now try to modify it. Enjoy matching quotation marks and juggling commas. It's awful, which is why everybody uses fmt.Sprintf() instead.
String interpolation is a must have these days, I wish the Go devs would wise up to that fact.
In Python, calling "{}".format(x) is string formatting, while string interpolation would be to use the language feature of "f-strings" such as f"{x}" to do the same thing. As far as I know, go doesn't have string interpolation, it only has convenient string formatting functions via the fmt package.
Basically, if you format strings with a language feature: interpolation. If you use a library to format strings: string formatting.
The difference is format strings are a string with indicators that say where to insert values, usually passed as additional arguments, which follow after the string. String interpolation has the arguments inside the string, which says how to pull the values out of the surrounding context.
Interpolation is where the value is placed directly in the string rather than appended as parameters.
Eg “I am $age years old”.
This does result in the side effect that interpolation is typically a language feature rather than a library feature. But there’s nothing from preventing someone writing an interpolation library, albeit you’d need a language with decent reflection or a one that’s dynamic from the outset.
That makes it a lot clearer where the problem is, which is also where the solution is: get the list of keys you want to work on ahead of time and iterate over those while modifying the map.
The upshot of this dogmatism is that its comparatively easy to dev on long-lived Go projects. If I join a new team with an old Go project, there's a very good chance that I'll be able to load it up in my IDE and get all of Go's excellent LSP, debug, linting, testing, etc. tooling going immediately. And when I start reading the code, its likely not going to look very different from a new Go project I'd start up today.
(BTW Thanks OP for these subtleties, there were a few things I learned about).
The one thing I wish Go had more than anything is read-only slices (like C#).
The one thing I wish more other languages had that Go has is structural typing (anything with Foo() method can be used as an interface { Foo() }.
On the other hand, now that we have iterators in Go, you can create a wrapper for []byte that only allows reading, yet is iterable.
But then we're abstracting away, which is a no-go in Go and also creates problems later on when you get custom types with custom logic.
My guess is that it is due to many developers bringing reference semantics with them from other languages to Go. It leads to thinking about data in terms of pointers instead of values.
ReadOnlySpan<T> in C# is great! In my opinion, Go essentially designed in “span” from the start.
Interesting approach regarding using strings as containers for raw bytes, but when you create one over a []byte I believe it makes a copy almost always (always?) so you can’t get a zero-cost read-only view of the data to pass to other functions.
One way that you will find it is that they used to be called open arrays in some of them.
type User struct {
Name string `json:"name"`
Password string `json:"-"`
Email string `json:"email"`
}
So you can specify how to serialize a struct in json using raw string literals containing arbitrary metadata. And json:"X" means to serialize it to X, except the special value "-" means "omit this one," except "-," means that its name is "-". Got it.An alternative is to introduce something like annotations, but I'm sure there will be resistance as it makes the language lean closer to e.g. Java.
But my take on that is that if you want stricter typing like that, you should actually go to Java or C# or whatever.
That and one or two other examples in the article smelled vaguely of PHP to me: features piled up in response to immediate needs instead of coherent design. For a language that famously refused to add generics for years (then did them badly, IMHO), it seems off-brand.
Support for the types of metaprogramming/metadata that annotations are used for is a useful attribute of languages in general
I was so surprised by the design choice to need to put recover in in deferred function calls. It’s crazy to smush together the error handling and normal execution code.
Assuming recover has to exist, I think forcing it to be in a deferred function is genius because it composes so well with how defers work in go. It's guaranteed to run "when the function returns" which is exactly the time to catch such truly catastrophic behaviors.
Until go1.23 [0], Recover() comes in handy for fault reports, however; ex: https://github.com/hashicorp/terraform/blob/325d18262e/inter...
[0] which introduced debug.SetCrashOutput: https://pkg.go.dev/runtime/debug#SetCrashOutput
func Foo() { try { maybePanic() } catch (err any) { doSomething(err) }
.. more code
}vs
func Foo() { defer func() { if err := recover(); err != nil { doSomething(err) } }()
maybePanic()
.. more code
}This is why they’re commonly used to signal on channels when you don’t actually have to send any data. Compare this to booleans, which still must occupy some space.”
I would expect the compiler to ensure that all references to true and false reference single addresses, too. So, at best, the difference of the more obscure code is to, maybe, gain 8 bytes. What do I overlook?
If you have a buffered channel with 100 "struct{}{}"s in it, you only need to store the length, since the element type is zero-sized.
Why? If you're sending a constant true to a channel, wouldn't that true value exist in the stack frame for the function call? It seems like that would make more sense than a pointer to a constant true value being stored in the stack frame and dereferencing that every time you need the constant value.
> So, at best, the difference of the more obscure code is to, maybe, gain 8 bytes. What do I overlook?
Constructing channels in a loop would potentially multiply memory usage here
That means it would work if *bool is possible but it's not.
My post https://news.ycombinator.com/item?id=44982491 got a lot of hate from people who defend Go by saying "so just don't do that!", and people trying to explain my own blog post to me.
If you read & write Go regularly, the rather verbose error handling simply fades into the background.
That said, errors in Go don't really translate to Exceptions as generally thought of; panic, however; may be does.
Making changes to error handling wasn't for the lack of trying, though: https://news.ycombinator.com/item?id=44171677
> issue with nil pointers
This is why most APIs strive for a non-nil zero value, where possible, as methods (on structs) can still dictate if it will act on a pointer. Though, I get what you're saying with Go missing Optional / Maybe / ? operator, as the only other way to warn about nil types is through documentation; ex: https://github.com/tailscale/tailscale/blob/afaa23c3b4/syncs... (a recent example I stumbled upon).
Static code analysers like nilaway (https://news.ycombinator.com/item?id=38300425) help, but these aren't without false positives (annoying) & false negatives (fatal).
Then again that would mean that the nil identifier would be coerced into a typed nil and we would check for the nilness of what is inside an interface in any(somepointer) == nil.
wrt the current behavior, it also makes sense to have a nil value that remains untyped. But in many other cases we do have that automatic inference/coercion, for instance when we set a pointer to nil.(p = nil)
That's quite subtle and that ship has sailed though.
But any(nil) == nil returns true like you'd expect.
The reason that any((*int)(nil)) == nil is false is the same reason that any(uint(2)) == 2 is false: interfaces compare values and types.
This is fine for a lot of general purpose code that exits when running into problems. But when errors are an expected part of a long lived process, like an API, it’s painful to build logic around and conditionally handle them.
The ergonomics of errors.Is and As are pretty bad and there doesn’t seem to be a clear indication as when to expect a sentinel, concrete, or pointer to a concrete error.
All that to say, I think Go’s errors really illustrate the benefit of “return values, not interfaces”. Though for errors specifically, I’m not sure you could improve them without some pretty bad tradeoffs around flexibility.
Loudest arguments against returning concrete types were on the terraform core team and the excuse was it makes testing easier. I disagree.
I find that people try to use interfaces like they’re using an OO language. Go is not OO.
It’s really elegant acquisition by reading, and releasing the semaphore by writing.
Great to limit your rest / http crawlers to 8 concurrent calls like a web browser.
One problem with using a channel as a semaphore is you need to track if you've closed the channel when "releasing".
https://pkg.go.dev/golang.org/x/sync/semaphore#Weighted.Acqu...
A lot of people praise it for it's "simplicity" and "explicitness" but frankly, even just figuring out whether something is being passed by reference or value is often complicated. If you're writing code where you never care about that, sure. But for any real project it's not actually better or simpler than C++ or Python.
Try utf8.RuneCountInString().
The time.After function creates a channel that will be sent a message after x seconds.
Or, will it? https://github.com/golang/go/issues/24595 ... even though the value is nil, the type of the variable is a non-nil interface... Go "boxes" that value in an interface, which is not nil. This can really bite you if you return interfaces from functions
Bit me when I was noob. These days, I fail build if ireturn fails.go install github.com/butuzov/ireturn/cmd/ireturn@latest ireturn ./...
Go 1.25 introduced a waitgroup.Go function that lets you add Go routines to a waitgroup more easily.
sync.WaitGroup(n) panics if Add(x) is called after a Done(-1) & n isn't now zero. Unsure if WaitGroups and easy belong in the same sentence. May be they do, but I'd rather reimplement Java's CountDownLatch & CyclicBarrier APIs in Go instead. When you embed structs, you also implicitly promote any methods they contain ... Say, for instance, you embed a time.Time struct onto a JSON response field and try to marshal that parent ... Since the time.Time method has a MarshalJSON() method, the compiler will run that over the regular marshalling behavior
#£@&+!Genuinely asking, I'm relatively new to Golang and would love to have a better sense of what parts of the ecosystem are worth learning about.
They will get cleaned up.
> Runes correspond to code points in Go, which are between 1 and 4 bytes long.
That's the dumbest thing I've read in this month. Why did they use the wrong word, sowing confusion¹, when any other programming language and the Unicode standard uses the correct expression "code point"?
¹ https://codepoints.net/runic already exists
Actually no, these are Unicode scalars, not code points; they exclude the surrogate category.
I agree that rune is a very poor name for it. It both mistakes what runes actually are and clashes with the runic block. But C# has adopted the Rune name for some reason.
Rust simply calls these char, and OCaml uchar (unicode char), which are much better choices.
Your use of the fallacy falls short of the reasoning standard expected here on HN. I did not downvote you, because I'd rather engage with words and effect change, but it does not surprise me that someone else did.