Live data from Hacker News

Go subtleties

harrisoncramer.me

91–100 of 190 posts

Re: Go subtleties

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

Re: Go subtleties

#92

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 ma…

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.

Re: Go subtleties

#93
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?

Re: Go subtleties

#94

Earlier quoted context omitted.

In Go, string effectively serves as a read-only slice, if we are talking about bytes. ReadOnlySpan in C# is great! In my opinion, Go essentially designed in “span” from the start.

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.

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

Re: Go subtleties

#95
post #80
post #71

Earlier quoted context omitted.

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…

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.

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

Re: Go subtleties

#97
post #87

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.

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.

Re: Go subtleties

#98
post #9

Earlier quoted context omitted.

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…

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.

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/

Re: Go subtleties

#99

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 ma…

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…

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.

Post reply on HN