Go subtleties

(harrisoncramer.me)

191 points | by darccio 9 days ago

24 comments

  • acatton 6 hours ago
    > 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 6 hours ago
      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 2 hours ago
        > 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.

    • mholt 4 hours ago
      The extended standard lib is pretty great, but definitely can't keep the Go compatibility promise, so it's good that it's separate.
    • lagniappe 5 hours ago
      >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 1 hour ago
        I do believe it’s backwards compatibility and evolving APIs
      • CamouflagedKiwi 59 minutes ago
        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.
    • infogulch 1 hour ago
      Wow how did I not know of this?!

      How does it cancel in-progress goroutines when the provided context is cancelled?

      • wesleyd 1 hour ago
        They have to all use the special context.
    • h4ck_th3_pl4n3t 2 hours ago
      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.

  • mwsherman 4 hours 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

  • tapirl 3 hours 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...

  • Groxx 4 hours 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

  • valzam 9 hours 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.
    • jbreckmckye 2 hours ago
      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 1 hour ago
        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

    • valzam 9 hours ago
      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 9 hours ago
        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 8 hours ago
          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 7 hours ago
            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.

            • 9rx 56 minutes ago
              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.

            • ttz 1 hour ago
              out of curiosity (not meant snidely), do you have an example of a case where the weaker type system resulted in serious problems?
              • DanielHB 41 minutes ago
                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.

          • kace91 5 hours ago
            >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 5 hours ago
            >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 2 hours ago
          > 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 25 minutes ago
            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.
      • eweise 4 hours ago
        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.
      • gethly 1 hour ago
        > 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.

      • h4ck_th3_pl4n3t 2 hours ago
        Oh boi, all my RW mutexes for maps across goroutines would disagree.
        • gethly 1 hour ago
          Use sync.Map or create simple wrapper to control access.
      • usrnm 9 hours ago
        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 1 hour ago
          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.
  • DarkNova6 8 hours 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“.
    • OvervCW 8 hours ago
      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.

      • pjmlp 8 hours ago
        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

      • DanielHB 7 hours ago
        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 5 hours ago
        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 5 hours ago
          How much is premature in time? 10 years? 20, 30 years?
          • liampulles 5 hours ago
            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.

      • eptcyka 8 hours ago
        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 8 hours ago
          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.
      • petralithic 7 hours ago
        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 6 hours ago
          Sounds Like „Tell me about Generics in Go without telling me about Generics in Go“
          • OvervCW 5 hours ago
            No need to talk about generics when we can talk about something simple like the inability to implement type safe enums in Go.
      • 0x696C6961 8 hours ago
        If you think Go and C are that similar then you don't know either.
        • OvervCW 7 hours ago
          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 5 hours ago
            > 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 5 hours ago
              >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 4 hours ago
                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.

        • quietbritishjim 7 hours ago
          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 5 hours ago
            Pike was not involved with the design of C. He was involved with Newsqueak and Limbo which inspired Go's concurrency model.
    • tptacek 1 hour ago
      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 8 hours 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

  • Someone 3 hours 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?

    • Zambyte 3 hours ago
      > 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

    • tczMUFlmoNk 1 hour ago
      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.

    • h4ck_th3_pl4n3t 2 hours ago
      Go is copy by default.

      That means it would work if *bool is possible but it's not.

      • giancarlostoro 2 hours ago
        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.
    • arccy 2 hours ago
      it's also so that there's no confusion about what the value represents.
  • jasonthorsness 4 hours 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() }.

    • gethly 1 hour ago
      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 46 minutes ago
        > 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.

    • mwsherman 4 hours ago
      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 4 hours ago
        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 3 hours ago
          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

  • liampulles 4 hours 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).

  • furyofantares 3 hours 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.

    • jerf 2 hours ago
      "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 1 hour ago
      Mutating maps during iteration is a big red flag.
      • furyofantares 51 minutes ago
        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.

    • lelandbatey 3 hours ago
      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.

      • furyofantares 3 hours ago
        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.

      • hnlmorg 3 hours ago
        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.

  • johnmaguire 3 hours ago
    > Using len() with Strings, and UTF-8 Gotchas

    Try utf8.RuneCountInString().

    • gethly 1 hour ago
      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.
  • the_mitsuhiko 3 hours 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 :)
  • fweimer 3 hours ago
    Isn't using time.After for timeouts a bit of an anti-pattern? There is no way to cancel the pending computation.
  • callc 4 hours 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.

    • gethly 1 hour ago
      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
      }
    • lelandbatey 3 hours ago
      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.

  • porridgeraisin 10 hours 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`

  • rowanseymour 10 hours 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.
    • thomashabets2 5 hours ago
      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.

    • amelius 9 hours ago
      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 9 hours ago
        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 8 hours ago
          The “simplicity” of Go is just virtue signaling. It has gotchas like that all over the language, because it’s not actually simple.
          • LandR 7 hours ago
            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.

          • ignoramous 9 minutes ago
            > 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

          • laumars 4 hours ago
            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 4 hours ago
              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 4 hours ago
                > 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 8 hours ago
        > 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/syncs... (a recent example I stumbled upon).

        Static code analysers like nilaway (https://news.ycombinator.com/item?id=38300425) help, but these aren't without false positives (annoying) & false negatives (fatal).

    • aatd86 9 hours ago
      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.

      • kbolino 6 hours ago
        > 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 5 hours ago
          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 4 hours ago
            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 4 hours ago
              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 3 hours ago
                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 2 hours ago
                  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 2 hours ago
                    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 1 hour ago
                      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 24 minutes ago
                        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).
      • rowanseymour 9 hours ago
        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.
        • jerf 3 hours ago
          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/

          • ngrilly 47 minutes ago
            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.
          • rowanseymour 3 hours ago
            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 2 hours ago
              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 2 hours ago
                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!
        • aatd86 7 hours ago
          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 7 hours ago
            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 5 hours ago
              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...

    • kitd 9 hours ago
      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.
      • Magnolia9438 7 hours ago
        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.

      • leetrout 8 hours ago
        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 8 hours ago
          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 7 hours ago
            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 7 hours ago
              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 7 hours ago
            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 8 hours ago
        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.
        • bpicolo 7 hours ago
          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 1 hour ago
            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.
        • formerly_proven 7 hours ago
          > 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).

      • YesThatTom2 8 hours ago
        Exactly.

        I find that people try to use interfaces like they’re using an OO language. Go is not OO.

  • tonymet 3 hours 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.

  • formerly_proven 8 hours 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.

  • ale42 4 hours ago
    Did anyone else read "Go subtitles" instead of the actual title?
  • username223 7 hours 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.
    • Cthulhu_ 7 hours ago
      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 7 hours ago
        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 7 hours ago
        "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 7 hours ago
      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 1 hour ago
      Annotations have been part of programming for ages. This is nothing new or out of the ordinary functionality.
  • ignoramous 9 hours 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
    
    #£@&+!
    • dontlaugh 8 hours ago
      waitgroup's inadequacy is why I end up using structured concurrency like https://github.com/sourcegraph/conc
      • pcthrowaway 6 hours ago
        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 1 hour ago
          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.

  • bmn__ 4 hours 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