Go subtleties

darccio | 244 points

> The wg.Go Function

> 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.

acatton | 2 days ago

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.

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

mwsherman | 2 days ago

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`

porridgeraisin | 2 days ago

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.

valzam | 2 days ago

>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

Groxx | 2 days ago

As somebody who only views Go from a distance, I see this list as a combination of „what‘s the big deal?“ and „please don‘t“.

DarkNova6 | 2 days ago

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.

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...

tapirl | 2 days ago

I balked a little when the article refers to format strings as "string interpolation" but there's multiple comments here running with it. Am I out of date and we just call that string interpolation these days?

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.

furyofantares | 2 days ago

> This is helpful if you have to interpolate the same value multiple times and want to reduce repetition and make the interpolation easier to follow.

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

voidUpdate | 2 days ago

Go's subtle footguns are definitely its worst aspect. I say that as a "Go fanboy" (I confess). But I think its also worth asking WHY many of these footguns continue to exist from early Go versions - and the answer is that Go takes versioning very seriously and sticking to major version 1 very seriously.

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).

liampulles | 2 days ago

Great list! Reminds me to check out more of the new stuff in 1.25.

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() }.

jasonthorsness | 2 days ago

I had a “wtf” moment when using Go around panic() and recover()

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.

callc | 2 days ago

FTA: “In Go, empty structs occupy zero bytes. The Go runtime handles all zero-sized allocations, including empty structs, by returning a single, special memory address that takes up no space.

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?

Someone | 2 days ago

My opinion after using go professionally for ~2 years and repeatedly running into gotchas such as https://go.dev/blog/loopvar-preview is that it's just not a good language.

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.

ball_of_lint | 2 days ago

my favorite go trick is a simple semaphore using make(chan struct{}, CONCURRENCY) to throttle REST api calls and other concurrent goroutines.

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.

tonymet | 2 days ago

Ah the old nil values boxed into non-nil interfaces. Even after 8 years writing go code almost every day this still bites me occasionally. I've never seen code that actually uses this. I understand why it is the way it is but I hate it.

rowanseymour | 2 days ago

> Using len() with Strings, and UTF-8 Gotchas

Try utf8.RuneCountInString().

johnmaguire | 2 days ago

One of the cooler things in Go these days is that the new function based iterators are based on coroutines, and you can use the iter.Pull function to abuse that :)

the_mitsuhiko | 2 days ago

Isn't using time.After for timeouts a bit of an anti-pattern? There is no way to cancel the pending computation.

fweimer | 2 days ago

> 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.

formerly_proven | 2 days ago

Did anyone else read "Go subtitles" instead of the actual title?

ale42 | 2 days ago

Go has certainly come a long ways from its initial mission to be a simple language for Rob Pike's simple coworkers.

    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.
username223 | 2 days ago
[deleted]
| 2 days ago

  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
#£@&+!
ignoramous | 2 days ago

FTA:

> 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

bmn__ | 2 days ago