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