[HN Gopher] Go subtleties
       ___________________________________________________________________
        
       Go subtleties
        
       Author : darccio
       Score  : 212 points
       Date   : 2025-10-13 07:42 UTC (9 days ago)
        
 (HTM) web link (harrisoncramer.me)
 (TXT) w3m dump (harrisoncramer.me)
        
       | rowanseymour wrote:
       | 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.
        
         | amelius wrote:
         | I ditched Go after an evaluation years ago. I can remember it
         | was an issue with nil pointers being non-intuitive that turned
         | me off. And exception handling. A pity because the runtime and
         | ecosystem/community seemed pretty good.
        
           | rowanseymour wrote:
           | It's fantastic concise language and standard library steered
           | by people who are determined to keep it simple and
           | intuitive... which IMO makes it all the more odd that it has
           | this obvious foot gun trap where `!= nil` doesn't always mean
           | what you might think.
        
             | amw-zero wrote:
             | The "simplicity" of Go is just virtue signaling. It has
             | gotchas like that all over the language, because it's not
             | actually simple.
        
               | LandR wrote:
               | Yep.
               | 
               | The lack of features means all the complexity is
               | offloaded to the programmer. Where other languages can
               | take some of the complexity burden off the programmer.
               | 
               | Go isn't simple, it's basic.
        
               | amelius wrote:
               | Perhaps Go is a nice target language for a transpiler, so
               | you could still benefit from the runtime and ecosystem
               | while fixing the bugs in the language itself. Anyone
               | working on this?
        
               | bobbylarrybobby wrote:
               | https://github.com/borgo-lang/borgo
        
               | laumars wrote:
               | As someone who's written commercial software in well over
               | a dozen different languages for nearly 40 years, I
               | completely disagree.
               | 
               | Go has its warts for sure. But saying the simplicity of
               | Go is "just virtue signaling" is so far beyond ignorant
               | that I can only conclude this opinion of yours is nothing
               | more than the typical pseudo-religious biases that lesser
               | experienced developers smugly cling to.
               | 
               | Go has one of the easiest tool chains to get started.
               | There's no esconfig, virtualenv and other bullshit to
               | deal with. You don't need a dozen `use` headers just to
               | define the runtime version nor trust your luck with a
               | thousand dependencies that are impossible to
               | realistically audit because nobody bothered to bundle a
               | useful standard library with it. You don't have multi-
               | page indecipherable template errors, 50 different ways to
               | accomplish the same simple problem nor arguments about
               | what subset of the language is allowed to be used when
               | reviewing pull requests. There isn't undefined behaviour
               | nor subtle incompatibilities between different runtime
               | implementations causing fragmentation of the language.
               | 
               | The problem with Go is that it is _boring_ and that 's
               | boring for developers. But it's also the reason why it is
               | simple.
               | 
               | So it's not virtue signaling at all. It's not flawless
               | and it's definitely boring. But that doesn't mean it
               | isn't also simple.
               | 
               | Edit: In case anyone accuses me of being a fanboy, I'm
               | not. I much preferred the ALGOL lineage of languages to
               | the B lineage. I definitely don't like a lot of the
               | recent additions to Go, particularly around range
               | iteration. But that's my personal preference.
        
               | bobbylarrybobby wrote:
               | You are comparing Go to Python, JS, and C++, arguably the
               | three most complex languages to build. (JS isn't actually
               | _hard_ , but there are a lot of seemingly arbitrary
               | decisions that have to be made before you can begin.)
               | There are languages out there that are easy to build,
               | have a reasonable std lib, and don't offload the
               | complexity of the world onto the programmer.
        
               | laumars wrote:
               | > You are comparing Go to Python, JS, and C++, arguably
               | the three most complex languages to build.
               | 
               | No, I'm comparing to more than a dozen different
               | languages that I've used commercially. And there were
               | direct references there to Perl, Java, Pascal, procedural
               | SQL, and many, many others too.
               | 
               | > There are languages out there that are easy to build,
               | have a reasonable std lib
               | 
               | Sure. And the existence of them doesn't mean Go isn't
               | also simple.
               | 
               | > and don't offload the complexity of the world onto the
               | programmer.
               | 
               | I disagree. Every language makes tradeoffs, and those
               | tradeoffs always end up being complexities that the
               | programmer has to negotiate. This is something I've seen,
               | _without exception_ , in my 40 years of language
               | agnosticism and part-time language designer.
        
               | ignoramous wrote:
               | > _because it's not actually simple_
               | 
               | Cue Rich Hickey's _Simple made Easy_ :
               | https://www.youtube-nocookie.com/embed/SxdOUGdseq4 /
               | https://ghostarchive.org/varchive/SxdOUGdseq4
        
           | ignoramous wrote:
           | > _And exception handling_
           | 
           | 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/sy
           | ncs... (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).
        
         | aatd86 wrote:
         | Yes, that'a bit too late after ten+ years perhaps but I wished
         | we had a nil type and checking whether the interface is empty
         | was a type assertion. In all other cases, like any(2) == 2, we
         | compare the values.
         | 
         | 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.
        
           | rowanseymour wrote:
           | Agree the ship has likely sailed, but if it could be
           | addressed wouldn't it be nice to remove nil value interfaces
           | altogether? Maybe start by letting new interface types
           | declare/annotate that they don't box nil values? Then one day
           | that becomes the default. Oh well.
        
             | aatd86 wrote:
             | Oh that's probably doable. Introducing something like this
             | is a bit orthogonal to the point above, but yes.
             | 
             | It's not straightforward but probably something that will
             | be considered at some point I reckon when thinking about
             | making union interfaces first class. That will require to
             | track a not nil typestate/predicate in the backend,
             | something like that I guess.
        
               | rowanseymour wrote:
               | Having pondered on a bit more.. I think it's the struct
               | that would declare that it's not usable as nil, and that
               | in turn would tell the runtime not to box it if it's nil.
               | That would also help the compiler (and copilot etc) spot
               | calls on nil pointers which will panic.
        
               | aatd86 wrote:
               | But that information disappears when you assign to an
               | interface variable/container that is nillable. It
               | requires an assertion to recover the info about the value
               | inside the interface being not nil.
               | 
               | basically `if v.(nil){...}
               | 
               | creates two branches. In one we know v is not nil
               | (outside the if block) and it can therefore be assigned
               | to non nillable variables so to speak...
        
             | jerf wrote:
             | It's not that the ship has sailed, it is that if you sit
             | down and sketch out what people think they want it is
             | logically incoherent. What Go does is the logically-
             | coherent result of the way interfaces work and the fact
             | that "nil" values _are not invalid_. It is perfectly legal
             | for a  "nil" pointer to validly implement an interface. For
             | instance, see https://go.dev/play/p/JBsa8XXxeJP , where a
             | nil pointer of "*Repeater" is a completely valid
             | implementation of the io.Reader interface; it represents
             | the "don't repeat anything at all" value.
             | 
             | In light of that fact, it would cause the interface rules
             | to grow a unique wart that doesn't accomplish anything if
             | interfaces tried to ban putting "nil" pointers into them.
             | The correct answer is to not to create invalid values in
             | the first place [1] and basically "don't do that", but
             | that's not a "don't do that because it ought to do what you
             | think and it just doesn't for some reason", it's a "don't
             | do that because what you think should happen is in fact
             | wrong and you need to learn to think the right thing".
             | 
             | Interfaces can not decide to not box nil values, because
             | interfaces are not supposed to "know" what is and is not a
             | legal value that implements them. It is the responsibility
             | of the code that puts a value into the interface to ensure
             | that the value correctly implements the interface. Note how
             | you could not have io.Reader label itself as "not
             | containing a nil" in my example above, because io.Reader
             | has no way to "know" what my Repeater is. The job of an
             | io.Reader value is to Read([]byte) (int error), and if it
             | can't do that, it is not io.Reader's "fault". It is the
             | fault of the code that made a promise that some value fits
             | into the io.Reader interface when it doesn't.
             | 
             | In Go, _nil is not the same thing as invalid_ [2] and until
             | you stop forcing that idea into the language from other
             | previous languages you 've used you're going to not just
             | have a bad time here, but elsewhere as well, e.g., in the
             | behavior of the various nil values for slice and map and
             | such.
             | 
             | One can more justifiably make the complaint that there is
             | often no easy way to make a clearly-invalid value in Go the
             | way a sum type can clearly declare an
             | "Invalid/None/Empty/NULL", or even declare multiple such
             | values in a single type if the semantics call for it, but
             | that's a separate issue and doesn't make "nil" be the
             | invalid value in current Go. Go does not have a dedicated
             | "invalid" value, nor does it have a value of a given type
             | that methods can not be called on.
             | 
             | (You can also ask for Go to have more features that make it
             | harder to stick invalid values into an interface, but if
             | you try to follow that to the point where it is literally
             | impossible, you end up in dependently-typed languages,
             | which currently have no practical implementations. Nothing
             | can prevent you, in any current popular language, from
             | labelling a bit of code as implementing an
             | interface/trait/set of methods and simply being wrong about
             | that fact. So it's all a question of where the tradeoffs
             | are in the end, since "totally accurately correct
             | interfaces" are not currently known to even be possible.)
             | 
             | [1]: https://jerf.org/iri/post/2957/
             | 
             | [2]: https://jerf.org/iri/post/2023/value_validity/
        
               | rowanseymour wrote:
               | I don't know why every time people complain about this
               | there is an assumption that we just don't understand why
               | it is the way it is. I get that x can implement X and x
               | can have methods that work with nil. I sometimes write
               | methods that work with nils. It's a neat feature.
               | 
               | What's frustrating is that 99.99% of written go code
               | doesn't work this way and so people _do_ shoot themselves
               | in the foot all the time, and so at some point you have
               | to concede that what we have might be logical but it
               | isn't intuitive. And that kinda sucks for a language that
               | prides itself on simplicity.
               | 
               | I also get that there's no easy way to address this. The
               | best I can imagine is a way to declare that a method Y on
               | type x can't take nil so (*x)(nil) shouldn't be
               | considered as satisfying that method on an interface..
               | and thus not boxed automatically into that interface
               | type. But yeah I get that's gonna get messy. If I could
               | think of a good solution I'd make a proposal.
        
               | jerf wrote:
               | Because in the last dozen times I've handled this
               | question the root cause is lack of understanding of why.
               | Inductively it is logical to conclude that's the reason
               | next time. It is probably also the case the bulk of
               | readers of this conversation are still in the camp that
               | don't understand the problem correctly.
               | 
               | If you understand that there isn't really a fix and just
               | wish there was one anyhow, while I still disagree in some
               | details it's in the range I wouldn't fuss about. I
               | understand that sort of wishing perfectly; don't think
               | there's ever been a language I've used for a long time
               | that I've had similar sorts of "I just wish it could work
               | this way even though I understand why it can't." Maybe
               | someday we'll be "blessed" with some sort of LLM-based
               | language that can do things like that... for better or
               | for worse.
        
               | rowanseymour wrote:
               | I can't think of good way to give programmers control
               | over boxing without adding a bunch of complexity that
               | nobody wants.. but it doesn't seem out of the realm of
               | possibility that the linter could detect issues like
               | this. It should be able to spot methods that aren't nil-
               | safe and spot nil values of those types ending up in
               | interfaces with those methods. Then you'd have less
               | explaining to do!
        
               | ngrilly wrote:
               | Your blog posts (that I read a few weeks ago) and your
               | comment here are the best explanations I've ever read on
               | this topic. You're not just looking at the surface of the
               | problem, but diving in the why it is like that,
               | semantically. I really like that you mentioned dependent
               | typing in your conclusion.
        
           | kbolino wrote:
           | > In all other cases, like any(2) == 2, we compare the
           | values.
           | 
           | 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_.
        
             | aatd86 wrote:
             | that's another thing that makes it difficult to fix. Same
             | thing here. 2 is an untyped constant so it should have
             | returned true. (even if int is the default picked on short
             | assignment)
             | 
             | any(uint(2)) == int(2) should return false indeed however.
        
               | kbolino wrote:
               | Untyped constants deserve an entry of their own in a list
               | of the language's subtleties, that's for sure.
               | 
               | Importantly, untyped constants don't exist at runtime,
               | and non-primitive types like interfaces aren't constants,
               | so any(uint(2)) == 2 can't behave the way you want
               | without some pretty significant changes to the language's
               | semantics. Either untyped constants would have to get a
               | runtime representation--and equality comparisons would
               | have to introduce some heavyweight reflection--or else
               | interfaces would have to be hoisted into the constant
               | part of the language--which is quite tricky to get right
               | --and then you just end up in a situation where
               | any(uint(2)) == 2 works but x == 2 doesn't when x turns
               | out to be any(uint(2)) at runtime.
        
               | aatd86 wrote:
               | Not sure that reflection would be needed. They are
               | exclusively on the RHS. But you're right. They would have
               | a sort of type of their own instead of basically being
               | int under the hood. type conversions do not require
               | reflection. Or maybe you are thinking about something I
               | have overlooked? In any case, not very likely a change
               | anyway.
        
               | kbolino wrote:
               | Let's assume the runtime representation case, as it's the
               | most flexible. You'd need to do an assignability check to
               | compare it to a typed number. Keep LHS as the interface,
               | and RHS as the untyped constant.
               | 
               | That means following the type pointer of LHS, switching
               | on its underlying type (with 15 valid possibilities [1])
               | or similar, and then casting either RHS to LHS's type, or
               | LHS to the untyped representation, and finally doing the
               | equality check. Something like this (modulo choice of
               | representation and possible optimizations):
               | import ("math/big"; "reflect")       type untypedInt
               | struct { i *big.Int }       func (x untypedInt) equals(y
               | any) bool {         val := reflect.ValueOf(y)         if
               | val.Type() == reflect.TypeOf(x) {           return
               | x.i.Cmp(val.Interface().(untypedInt).i) == 0         }
               | else if val.CanInt() {           if !x.i.IsInt64() {
               | return false }           return x.i.Int64() == val.Int()
               | } else if val.CanUint() {           if !x.i.IsUint64() {
               | return false }           return x.i.Uint64() ==
               | val.Uint()         } else {           var yf float64
               | if val.CanFloat() {             yf = val.Float()
               | } else if val.CanComplex() {             yc :=
               | val.Complex()             if imag(yc) != 0 { return false
               | }             yf = real(yc)           } else { return
               | false }           xf, acc := x.i.Float64()           if
               | acc != big.Exact { return false }           return xf ==
               | yf         }       }
               | 
               | [1]: Untyped integer constants can be compared with any
               | of uint8..uint64, int8..int64, int, uint, uintptr,
               | float32, float64, complex64, or complex128
        
               | aatd86 wrote:
               | If it is because of overflow, the idea was that there
               | could be size classes at compile time. A bit like
               | sub/supertyping but for numeric types. A simple type
               | pointer check would be sufficient.
        
               | kbolino wrote:
               | Size classes would save some space and speed up _like-to-
               | like_ comparisons, but wouldn 't really do much for
               | unlike comparisons (especially vs. float or complex).
               | Looking only at type pointers fails to account for custom
               | types (e.g., type Foo int); remember that an untyped
               | integer constant can be compared with these. If you want
               | the same semantics at runtime as you get at compile time,
               | I don't see how you can get much simpler than what I
               | wrote, in terms of the high-level logic. Though there are
               | undoubtedly ways to optimize it, both because Go's
               | compiler favors speed of compilation over efficiency of
               | generated code, and because if this were the real code,
               | it could poke at internals while my (probably working)
               | example has to rely on the public reflect package, which
               | is more abstract.
        
               | aatd86 wrote:
               | But the LHS can determine how it compares to the RHS when
               | the RHS is determined to be an untyped constant? Or
               | instead of saying RHS (my mistake), let's say the typed
               | side since comparisons are symmetric. A bit like having a
               | special method attached to the type strictly for
               | comparisons? That would be much less expensive than such
               | a type switch if I am not mistaken. Would handle custom
               | types as well. If promotable from the underlying type,
               | that would not even bloat the executable. Unless I'm
               | confused...
        
               | kbolino wrote:
               | Ok, I think I follow. Instead of putting the comparison
               | logic on the untyped side, you'd put it on the typed
               | side. In code, reusing imports and untypedInt
               | declaration, but replacing the method from before, you'd
               | have:                 type intType interface { ~int |
               | ~int8 | ~int16 | ~int32 | ~int64 }       func equals[I
               | intType](x I, y any) bool {         switch val :=
               | y.(type) {         case I: return x == val         case
               | untypedInt: return val.i.IsInt64() && val.i.Int64() ==
               | int64(x)         default: return false         }       }
               | 
               | And this would need a separate specialization for
               | unsigned integers, floats, and complex numbers. This
               | approach saves us from having to introspect the
               | underlying type at runtime, but the example is
               | incomplete. We also have float and complex untyped
               | constants, so now each concrete type has to switch on all
               | of the untyped constant forms it compares with. Still, it
               | might be faster, though I'm not sure how much it reduces
               | code bloat in practice (it's nice to not need the reflect
               | package though).
               | 
               | [edit: side note, I was trying to actually write out all
               | the code that would be needed, and I discovered that you
               | can't call real or imag on generic types:
               | https://github.com/golang/go/issues/50937]
        
         | kitd wrote:
         | The advice I've read (and follow) is always to return values,
         | not interfaces, from functions and test for nil against them.
         | That IME tends to nip the majority of nil interface problems in
         | the bud.
        
           | YesThatTom2 wrote:
           | Exactly.
           | 
           | I find that people try to use interfaces like they're using
           | an OO language. Go is not OO.
        
           | leetrout wrote:
           | Return concrete types, accept interfaces. Returning
           | interfaces hides behavior and hampers evolution; accept
           | interfaces so callers can swap implementations. For testing,
           | mock the dependency, not the return value.
           | 
           | Loudest arguments against returning concrete types were on
           | the terraform core team and the excuse was it makes testing
           | easier. I disagree.
        
             | lenkite wrote:
             | This advice of "returning concrete types" is in most cases
             | a horrible anti-pattern that prevents evolution due to lack
             | of information hiding. It has been also deliberately broken
             | in the standard library in several places. This "advice"
             | cannot be generically applied. Places where it is has been
             | deliberately broken:                  net.Dial (Conn,
             | error)         image.Decode(r io.Reader) (Image, string,
             | error)        sha256.NewXXX() hash.Hash
             | flate.NewReader(r io.Reader) io.ReadCloser
             | http.NewFileTransport(fs FileSystem) RoundTripper
             | 
             | Regarding `os.File`, the Go team even said: "If we were
             | starting from scratch, we might do it differently."
             | 
             | That's why Go added abstractions later like fs.FS and
             | fs.File.                  embed/fs.Open again deliberately
             | breaks this.
             | 
             | Whereas consider its counterpart net.Conn. net.Conn is one
             | of the most successful interfaces in the Go standard
             | library. It's the foundation of the net, net/http, tls, and
             | net/rpc packages, and has been stable since Go 1.0. It
             | didn't need a replacement fs.Fs.
             | 
             | If you will always only ever have one implementation in
             | absolute permanence and no mocking/fake/alternative
             | implementation is ever required in eternity, return a
             | concrete type. Otherwise, consider whether returning an
             | interface makes more sense.
        
               | arccy wrote:
               | On the contrary, in recent proposal reviews, returning
               | interfaces has been discouraged unless you're trying to
               | make a generic interface like fs.FS, or dispatch
               | functions like net.Dial / image.Decoder.
               | 
               | The advice of returning concrete types is paired with
               | defining interfaces when you need them on the consumer
               | side.
               | 
               | It's returning interfaces that prevents good evolution,
               | since the standard library will not add methods to
               | interfaces, it can only document things like: all current
               | standard library implementations additionally satisfy XXX
               | interfaces.
        
               | lenkite wrote:
               | It seems there is a dichotomy in the real implemented
               | world and your hypothetical advice world.
               | 
               | Due to lack of native support of defaults for optional
               | methods , many interfaces in Go are using hacks for
               | optional methods added by evolution.
               | 
               | The Value interface has a `IsBoolFlag()` optional method
               | not part of the interface signature
               | 
               | The other way for evolution is just add sub-interfaces.
               | Like `io.WriterTo` and `io.ReaderFrom` which are
               | effectively just extensions of `io.Writer` and
               | `io.Reader` with `WriteTo` and `ReadFrom` methods - which
               | are checked for in consumers like `io.Copy`.
               | 
               | Anyways, my point was specifically about generic
               | interfaces and alternative implementations, so it appears
               | you agree.
        
               | leetrout wrote:
               | Like anything else there are exceptions to the rule.
               | Pointing to the standard library is a weak position
               | because it is consistently inconsistent.
               | 
               | Go's standard library interfaces (like net.Conn) earned
               | their place.
               | 
               | Premature interfaces calcify mistakes and that's what the
               | guideline pushes back on.
        
           | rowanseymour wrote:
           | That works until 1) you don't want to export the value types
           | 2) the return values aren't simple structs but slices or maps
           | because []x is not a []X even if x implements X.
        
             | formerly_proven wrote:
             | > the return values aren't simple structs but slices or
             | maps because []x is not a []X even if x implements X.
             | 
             | I assume this is because on is an array of struct pointers
             | and the other is an array of fat pointers, since Go has
             | reified interfaces (unlike higher-level languages).
        
             | bpicolo wrote:
             | For 1/, you can return a struct value type without
             | exporting it. If it satisfies the receiving interface they
             | won't have a problem.
             | 
             | That's exactly the pattern I use for most Go development
        
               | kbolino wrote:
               | This affects discoverability, though. Your unexported
               | type won't have public documentation. So you end up
               | having to publish an interface anyway (even if you don't
               | return it) or document in words what the method set looks
               | like.
        
           | Magnolia9438 wrote:
           | On the happy path downstream, yes, and it does work really
           | well. But the error flow back upstream flips that, as errors
           | are returned as, often nested, interfaces.
           | 
           | 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.
        
         | thomashabets2 wrote:
         | Be prepared to be called a newbie for criticising typed nils.
         | 
         | 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.
        
       | porridgeraisin wrote:
       | 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`
        
       | valzam wrote:
       | 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 wrote:
         | 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.
        
           | theshrike79 wrote:
           | 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...
        
             | spoiler wrote:
             | I know it's not exclusive to Go or any language, but you
             | can most certainly write incomprehensible code in it. If
             | anything, expressiveness and proper abstractions can save
             | you from this.
             | 
             | I think people often get burnt by bad abstractions in
             | expressive languages, but it's not a problem of the
             | language, but the author's unfamiliarity with the tools at
             | their disposal.
             | 
             | If someone starts being clever with abstractions before
             | understanding the fundamentals, it can lead to badly
             | designed abstractions.
             | 
             | So I guess if there's less things to master, you can start
             | designing good abstractions sooner.
             | 
             | So, in my experience, if we invest time to truly understand
             | the tools at our disposal, expressive languages tend to be
             | a great boon to comprehension and maintenance.
             | 
             | But yes, there's definitely been times early in my career
             | where I abstracted before I understood, or had to deal with
             | other bad abstractions
        
               | DanielHB wrote:
               | I like to say this: "Only my code is allowed to be
               | clever"
               | 
               | But, on a serious note, I agree with you. Go lacks a lot
               | of power, especially in its type system, that causes a
               | ton of problems (and downtime) that in other languages is
               | trivial to prevent statically.
        
               | ttz wrote:
               | out of curiosity (not meant snidely), do you have an
               | example of a case where the weaker type system resulted
               | in serious problems?
        
               | DanielHB wrote:
               | Pretty much any null pointer deference error ever?
               | 
               | But it is hardly ever the weak type system that is at
               | fault, just good use of a stronger type system could have
               | prevented the issue.
               | 
               | Once you start to make "invalid states unpresentable" and
               | enforcing those states at the edges of your type system
               | suddenly a lot of bizarre errors don't happen anymore.
        
               | ttz wrote:
               | NPEs are also present in a lot of languages with
               | "stronger" type systems though. Is there a specific
               | language you're comparing against?
        
               | 9rx wrote:
               | Formal proof languages are pretty neat, but nobody really
               | uses them in the real world. Among the languages that
               | people actually use on a normal basis, even those that
               | claim to have extensive type systems, they still rely on
               | testing for most everything, and once you're relying on
               | testing anyway the type system isn't any kind of real
               | saviour.
               | 
               | There is, perhaps, some segment of the developer
               | community who believe that they are infallible and don't
               | need to write tests, but then have the type system
               | exclaim their preconceived notions are wrong, and then
               | come to love the type system for steering them in a
               | better direction, while still remaining oblivious to all
               | the things the incomplete type system is unable to
               | statically assert. But that's a rather bizarre place to
               | be.
        
               | tyre wrote:
               | Typescript's type system is a huge leap up from
               | JavaScript.
               | 
               | You still need tests for functionality (this function
               | does what it should) but the type system removes many
               | error cases automatically.
        
               | kace91 wrote:
               | >it's not a problem of the language, but the author's
               | unfamiliarity with the tools at their disposal.
               | 
               | If you have to share a codebase with a large group of
               | people with varying skill levels, limiting their ability
               | to screw up can definitely be a feature, which a language
               | can have or lack.
               | 
               | As always, it comes with tradeoffs. Would you rather have
               | the ability to use good, expressive abstractions or
               | remove the group's ability to write bad ones? It probably
               | depends on your situation and goals.
        
               | lagniappe wrote:
               | >most certainly write incomprehensible code in it
               | 
               | I've tried my best to make indecipherable go code and
               | failed. Do you have any examples?
        
             | ErroneousBosh wrote:
             | > 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.
             | 
             | A mate of mine did Comp Sci back in uni when First Years
             | were taught Turbo Pascal showed me some, when I was still
             | doing stuff in ZX Spectrum BASIC and Z80 assembler in high
             | school. It was immediately clear what was going on, even if
             | the syntax was a bit unfamiliar.
             | 
             | By contrast I've had to sit and pick apart things with
             | strings and strings of ternary operators in the same
             | expression, as part of case structures that relied on
             | fallthrough, because someone wanted to show how clever they
             | were.
             | 
             | My Pascal-using mate called stuff like that "Yngwie
             | Malmsteen Programming". It's a phrase that's stuck with me,
             | over 30 years later.
             | 
             | Don't do that "WEEDLYWEEDLYWEEDLY" shit. You're just
             | showing off.
        
               | theshrike79 wrote:
               | I always reiterate to junior programmers that you write
               | as clever code as you want.
               | 
               | On your own time.
               | 
               | When you're writing code for work, stuff that other
               | people have to eventually read and understand, you be as
               | boring as possible. Skip all the tricks and make code
               | readable, not cute. Someone might have to understand and
               | fix it at 3 in the morning while everything is on fire.
               | > Always code as if the guy who ends up maintaining your
               | code will be a violent psychopath who knows where you
               | live.
        
             | cyberax wrote:
             | Yeah, so pretty much every large project has concurrency
             | bugs in Go. Because before generics, there was no way to
             | write a parallel for-each loop without shooting yourself in
             | the foot a couple of times.
             | 
             | Go is simple just like assembly is simple.
        
           | usrnm wrote:
           | 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
        
             | gethly wrote:
             | Interface is just behavior. That is the main difference
             | from other languages. Go is about "what", not "who". So
             | when you are checking for nil, you are essentially asking
             | whether the variable has any logic it can perform. And that
             | can happen only if some behavior was provided, ie. it is
             | not nil.
        
           | eweise wrote:
           | 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.
        
           | h4ck_th3_pl4n3t wrote:
           | Oh boi, all my RW mutexes for maps across goroutines would
           | disagree.
        
             | gethly wrote:
             | Use sync.Map or create simple wrapper to control access.
        
               | mook wrote:
               | ... the documentation for sync.Map literally says:
               | 
               | > The Map type is specialized. Most code should use a
               | plain Go map instead, with separate locking or
               | coordination, for better type safety and to make it
               | easier to maintain other invariants along with the map
               | content.
               | 
               | The documentation basically says that it's optimized for
               | some cases that wouldn't affect the complaint above.
        
               | gethly wrote:
               | If you want to be pedantic, you can use
               | https://github.com/puzpuzpuz/xsync which has generics and
               | is faster than native sync.Map
        
           | gethly wrote:
           | > Go really rewards writing the dumbest code possible
           | 
           | Simplicity is hard. You may see it as dumb, other see it as
           | priceless attribute of the language.
        
         | jbreckmckye wrote:
         | As a Go learner, the best explanation that has made sense to me
         | is that interface types essentially just compose two pointers:
         | 
         | 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
        
           | whateveracct wrote:
           | ah yes of course - key semantics of `nil` should totally
           | depend on deep implementation details in my language's
           | runtime.
           | 
           | willem-dafoe-head-tap.gif
        
       | ignoramous wrote:
       | 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
       | 
       | #PS@&+!
        
         | dontlaugh wrote:
         | waitgroup's inadequacy is why I end up using structured
         | concurrency like https://github.com/sourcegraph/conc
        
           | pcthrowaway wrote:
           | The last release of this was 2.5 years ago, and it's
           | pre-1.0... is this really ready for production use?
           | 
           | 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.
        
             | hellcow wrote:
             | I used it in production for lots of systems, so yes. It's
             | pretty great!
             | 
             | That said 2.5 years later there's been many improvements to
             | the stdlib (like waitGroup.Go) such that I no longer feel
             | the need for it going forward.
        
       | DarkNova6 wrote:
       | 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".
        
         | OvervCW wrote:
         | 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.
        
           | eptcyka wrote:
           | How is the boxed interface problem enabling better ergonomics
           | for Go? I find that most quirks listed here are not making
           | the language any better.
        
             | OvervCW wrote:
             | In this specific blog post I suppose only "Ranging Directly
             | over Integers" counts, but I was more generally referring
             | to the introduction of features like generics.
        
           | 0x696C6961 wrote:
           | If you think Go and C are that similar then you don't know
           | either.
        
             | quietbritishjim wrote:
             | Go and C have partially shared origins. Two of the three
             | creators of Go (Ken Thompson and Rob Pike) were involved in
             | the early days of C. Ken Thompson is even the creator of B,
             | the predecessor of C. There are obvious huge differences
             | between the language but in a more subtle way they're
             | actually quite similar: C is an "unergonomically simplistic
             | language", just as the parent commenter describes Go.
        
               | 0x696C6961 wrote:
               | Pike was not involved with the design of C. He was
               | involved with Newsqueak and Limbo which inspired Go's
               | concurrency model.
        
             | OvervCW wrote:
             | They are similar in the sense that there are very few
             | abstractions, relying on the programmer to reimplement
             | common patterns and avoid logical mistakes.
             | 
             | You have to put thought into such things as:
             | 
             | - Did I add explicit checks for all the errors my function
             | calls might return?
             | 
             | - Are all of my resources (e.g. file handles) cleaned up
             | properly in all scenarios? Or did I forget a "defer
             | file.Close()"? (A language like C++ solved this problem
             | with RAII in the 1980s)
             | 
             | - Does my Go channel spaghetti properly implement a worker
             | pool system with the right semaphores and error handling?
        
               | 0x696C6961 wrote:
               | > Did I add explicit checks for all the errors my
               | function calls might return?
               | 
               | You can easily check this with a linter.
               | 
               | > Are all of my resources (e.g. file handles) cleaned up
               | properly in all scenarios? Or did I forget a "defer
               | file.Close()"? (A language like C++ solved this problem
               | with RAII in the 1980s)
               | 
               | You can forget to use `with` in Python, I guess that's
               | also C now too eh?
               | 
               | > Does my Go channel spaghetti properly implement a
               | worker pool system with the right semaphores and error
               | handling?
               | 
               | Then stop writing spaghetti and use a higher level
               | abstraction like `x/sync/errgroup.Group`.
        
               | OvervCW wrote:
               | >Did I add explicit checks for all the errors my function
               | calls might return?
               | 
               | You can check anything with a linter, but it's better
               | when the language disallows you from making the mistake
               | in the first place.
               | 
               | >You can forget to use `with` in Python, I guess that's
               | also C now too eh?
               | 
               | When using `with` in Python you don't have to think about
               | what exactly needs to be cleaned up, and it'll happen
               | automatically when there is any kind of error. Consider
               | `http.Get` in Go:
               | 
               | resp, err := http.Get(url)
               | 
               | if err == nil { resp.Body.Close() }
               | 
               | return err
               | 
               | Here you need to specifically remember to call
               | `resp.Body.Close` and in which case to call it.
               | Needlessly complicated.
               | 
               | >Then stop writing spaghetti and use a higher level
               | abstraction like `x/sync/errgroup.Group`.
               | 
               | Why is this not part of the standard library? And why
               | does it not implement basic functionality like collecting
               | results?
        
               | amiga386 wrote:
               | This seems like a judiciously designed API to me.
               | 
               | You don't need to check if err was nil before calling
               | resp.Body.Close()
               | 
               | https://pkg.go.dev/net/http#Get
               | 
               | > When err is nil, resp always contains a non-nil
               | resp.Body. Caller should close resp.Body when done
               | reading from it.
               | 
               | https://pkg.go.dev/net/http#Response
               | 
               | > The http Client and Transport guarantee that Body is
               | always non-nil, even on responses without a body or
               | responses with a zero-length body. It is the caller's
               | responsibility to close Body.
               | 
               | Calling http.Get() returns an object that symbolises the
               | response. The response body itself might be multiple
               | terabytes, so http.Get() shouldn't read it for you, but
               | give you a Reader of some sort.
               | 
               | The question then is, when does the Reader get closed?
               | The answer should be "when the caller is done with it".
               | This can't be automatic handled when the resp object goes
               | out of scope, as it would preclude the caller e.g.
               | passing the response to another goroutine for handling,
               | or putting it in an array, or similar.
               | 
               | Go _tooling_ is more than happy to tell you that there 's
               | an io.ReadCloser in one of the structs returned to you,
               | and it can see that you didn't Close() it, store it, or
               | pass it to somewhere else, before the struct it was in
               | went out of scope.
        
           | pjmlp wrote:
           | 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.
           | 
           | 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
        
           | petralithic wrote:
           | More like, let's throw away the last 75 years of programming
           | language theory advances, only to rediscover them again
           | ourselves, with much hardship.
        
             | DarkNova6 wrote:
             | Sounds Like ,,Tell me about Generics in Go without telling
             | me about Generics in Go"
        
               | OvervCW wrote:
               | No need to talk about generics when we can talk about
               | something simple like the inability to implement type
               | safe enums in Go.
        
           | DanielHB wrote:
           | 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).
        
           | liampulles wrote:
           | 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.
           | 
           | (This is of course all my experience; very IMO)
        
             | miohtama wrote:
             | How much is premature in time? 10 years? 20, 30 years?
        
               | liampulles wrote:
               | So for me, the question is: are these two things
               | intrinsically the same or coincidentally the same? If it
               | is intrinsically the same, then an
               | abstraction/centralization of logic is correct. If they
               | are coincidentally the same, then its better to keep them
               | separate.
               | 
               | Its premature if I don't know the answer to that question
               | with my current information, which is a common scenario
               | for me when I'm initially writing a new set of usecases.
               | 
               | If I get a 3rd copy of a thing, then its likely going to
               | become an abstraction (and I'll probably have better
               | understanding of the thing at the time to do that
               | abstraction). If I don't get a 3rd copy of that thing,
               | then its probably fine for the thing to be copied in 2
               | places, regardless of what the answer to my question is.
        
               | ted_dunning wrote:
               | On the other hand, when your abstraction has more
               | configuration options than methods, it is a sign that
               | (years ago) it really wasn't an abstract at all.
        
           | deivid wrote:
           | 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
        
         | tptacek wrote:
         | 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!
        
       | voidUpdate wrote:
       | > 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
        
       | formerly_proven wrote:
       | > 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.
        
       | username223 wrote:
       | 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.
        
         | Cthulhu_ wrote:
         | I never liked the concept of struct tags, it's a kind of
         | stringly typed programming where the meaning of X or - depends
         | entirely on what the json package says it means.
         | 
         | 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.
        
           | bpicolo wrote:
           | Java resisted first party support of annotations. It was a
           | very controversial addition in the early 2000s
           | 
           | Support for the types of metaprogramming/metadata that
           | annotations are used for is a useful attribute of languages
           | in general
        
           | username223 wrote:
           | "Stringly typed programming" is the phrase I was looking for.
           | It's the equivalent of a shrug from the programming language
           | designer: "I don't know what to do about that, so I'll just
           | add a magic string and let someone else figure it out."
           | 
           | 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.
        
         | rowanseymour wrote:
         | Of all the things one might critique Go for as not being
         | simple, I'm not sure this is it. I've never needed to serialize
         | "-" as a key in JSON but `-,` makes some sense given the
         | general pattern of field tags, e.g. `json:"name,omitempty"`
        
         | gethly wrote:
         | Annotations have been part of programming for ages. This is
         | nothing new or out of the ordinary functionality.
        
       | acatton wrote:
       | > 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.
        
         | blixt wrote:
         | 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!
        
           | giancarlostoro wrote:
           | > 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.
        
         | lagniappe wrote:
         | >golang.org/x/ is stuff that should be in the standard library
         | but isn't, for some reason
         | 
         | think of it as testing/staging before being merged into stable
         | stdlib
        
           | linhns wrote:
           | I do believe it's backwards compatibility and evolving APIs
        
           | CamouflagedKiwi wrote:
           | Except that it's a little bit too convenient, and highly
           | useful things like errgroup stay there instead of having been
           | adopted into the stdlib.
        
         | mholt wrote:
         | The extended standard lib is pretty great, but definitely can't
         | keep the Go compatibility promise, so it's good that it's
         | separate.
        
         | h4ck_th3_pl4n3t wrote:
         | 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.
        
         | infogulch wrote:
         | Wow how did I not know of this?!
         | 
         | How does it cancel in-progress goroutines when the provided
         | context is cancelled?
        
           | wesleyd wrote:
           | They have to all use the special context.
        
       | ale42 wrote:
       | Did anyone else read "Go subtitles" instead of the actual title?
        
       | jasonthorsness wrote:
       | 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() }.
        
         | mwsherman wrote:
         | In Go, string effectively serves as a read-only slice, if we
         | are talking about bytes.
         | 
         | ReadOnlySpan<T> in C# is great! In my opinion, Go essentially
         | designed in "span" from the start.
        
           | jasonthorsness wrote:
           | Yeah I think the C# team was definitely influenced by Go with
           | their addition of Spans..
           | 
           | 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.
        
             | mwsherman wrote:
             | That's true, converting in either direction will typically
             | allocate. Which it must, semantically.
             | 
             | One can use unsafe for a zero-copy conversion, but now you
             | are breaking the semantics: a string becomes mutable,
             | because its underlying bytes are mutable.
             | 
             | Or! One can often handle strings and bytes interchangeably
             | with generics: https://github.com/clipperhouse/stringish
        
         | gethly wrote:
         | Yeah, having mutability optional would be great. It would also
         | allow a lot of data to pass through the stack instead of heap
         | due to pointers, which Go is riddled with for absolutely no
         | reason(imo).
         | 
         | 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.
        
           | eikenberry wrote:
           | > Yeah, having mutability optional would be great. It would
           | also allow a lot of data to pass through the stack instead of
           | heap due to pointers, which Go is riddled with for absolutely
           | no reason(imo).
           | 
           | 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.
        
       | liampulles wrote:
       | 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).
        
       | Groxx wrote:
       | > _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
        
       | bmn__ wrote:
       | 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 confusion1, when any other programming
       | language and the Unicode standard uses the correct expression
       | "code point"?
       | 
       | 1 https://codepoints.net/runic already exists
        
         | debugnik wrote:
         | > uses the correct expression "code point"
         | 
         | 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.
        
       | mwsherman wrote:
       | 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
        
         | HeyImAlex wrote:
         | Here's my favorite post on the subject
         | https://adam-p.ca/blog/2025/04/string-length/
        
           | debugnik wrote:
           | Finally an article that doesn't pretend grapheme clusters are
           | the be-all end-all of Unicode handling.
           | 
           | 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.
        
         | virtualritz wrote:
         | len() is also returning int instead of uint/uint64 in Go.
         | 
         | 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.
        
       | callc wrote:
       | 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.
        
         | lelandbatey wrote:
         | It's cause it's not normal error handling to use recover(). In
         | smaller codebases, panic probably should not be present. For
         | larger codebases, recover should be in place only in very very
         | sparse locations (e.g. at the top level http handler middleware
         | to catch panics caused by unreliable code). But in general,
         | returning errors is supposed to be how all errors are signaled.
         | I've always loved the semantic distinction between panics vs
         | errors in go, they feel sooo much clearer than "normal"
         | exception handling (try ... catch) in other languages which
         | syntactically equivocate such common cases as "this file
         | doesn't exist" with "the program is misbehaving due to physical
         | RAM corruption". I think it's great that panic vs errors makes
         | that a brighter line.
         | 
         | 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.
        
           | ignoramous wrote:
           | _Recover()_ has its sharp edges. If some downstream function
           | part of the execution stack does not release a critical
           | resource because its execution was halted midway by the
           | bubbling _panic_ , then the resource will leak (and in the
           | case of a _locked_ sync.Mutex, subsequent _Lock()_ s will
           | deadlock).
           | 
           | 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
        
         | gethly wrote:
         | Semantics. Go at least does not restrict you to wrap every
         | single panicky call.
         | 
         | 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     }
        
       | furyofantares wrote:
       | 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.
        
         | lelandbatey wrote:
         | Indeed, I have always heard such techniques as "string
         | formatting" while built-in-to-the-language local-variable
         | implicit string formatting sugar syntax is the thing I've heard
         | called "string interpolation".
         | 
         | 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.
        
           | hnlmorg wrote:
           | Not quite.
           | 
           | 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.
        
           | furyofantares wrote:
           | I think that's usually how it breaks down in practice but
           | even if the language directly provided format strings, I'd
           | still call them format strings, and even if a library
           | provided string interpolation, I'd call it string
           | interpolation.
           | 
           | 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.
        
         | jerf wrote:
         | "Am I out of date and we just call that string interpolation
         | these days?"
         | 
         | 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.
        
         | gethly wrote:
         | Mutating maps during iteration is a big red flag.
        
           | furyofantares wrote:
           | Yep. Which is part of why I'd much rather talk accurately
           | about it being the iterator that may or may not encounter
           | newly added values.
           | 
           | 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.
        
       | tonymet wrote:
       | 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.
        
       | Someone wrote:
       | 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?
        
         | Zambyte wrote:
         | > I would expect the compiler to ensure that all references to
         | true and false reference single addresses, too.
         | 
         | 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
        
         | arccy wrote:
         | it's also so that there's no confusion about what the value
         | represents.
        
         | h4ck_th3_pl4n3t wrote:
         | Go is copy by default.
         | 
         | That means it would work if *bool is possible but it's not.
        
           | giancarlostoro wrote:
           | If they did it without you explicitly making bool a pointer,
           | then it would be syntactic sugar and it would kind of fall
           | away from the spirit of Go which is, if you look at a file
           | everything that's happening is known to you, there's no
           | metaprogramming witchcraft anywhere in sight usually.
        
         | tczMUFlmoNk wrote:
         | If you have a buffered channel with 100 "true"s in it, you're
         | using 100 bytes.
         | 
         | 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.
        
       | the_mitsuhiko wrote:
       | 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 :)
        
       | fweimer wrote:
       | Isn't using time.After for timeouts a bit of an anti-pattern?
       | There is no way to cancel the pending computation.
        
         | Philip-J-Fry wrote:
         | This was fixed in Go 1.23 https://go.dev/doc/go1.23#timer-
         | changes
         | 
         | They will get cleaned up.
        
           | fweimer wrote:
           | I meant that the goroutine in the example keeps running.
           | Often this contradicts the reason for having the timeout in
           | the first place.
        
             | Philip-J-Fry wrote:
             | I'm not sure what you mean? The examples all return from
             | their function.
        
       | tapirl wrote:
       | 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...
        
       | johnmaguire wrote:
       | > Using len() with Strings, and UTF-8 Gotchas
       | 
       | Try utf8.RuneCountInString().
        
         | gethly wrote:
         | Problem with this is that it requires the whole string to be
         | iterated over, byte by byte, or rune by rune, whereas len()
         | does no such thing as the length is stored in the underlying
         | type.
        
       | ball_of_lint wrote:
       | 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.
        
       ___________________________________________________________________
       (page generated 2025-10-22 23:01 UTC)