Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

Enums: yes, absolutely, good call

Universal Nil: no. we need less nil not more. Nil-safe types would be a better answer.

Error-checking: If you're not going to do anything with the error, then have an editor snippet for the ? boilerplate. Better still, start handling the error. The boilerplate is a Go anti-pattern.

I think we do need more clarity about the relationship between slices and arrays: it's not clear at the moment when two slices share a backing array or not, and therefore whether changes to one slice will change the other.

I'm kinda looking forward to generics, but also dreading it. Most newbie gophers don't understand how powerful duck-typed interfaces are and what they can do with them, and I have a feeling generics will be used for all sorts of situations that could be better handled with interfaces.



It's not universal nil, it's more universal zero. This proposal, in one way or another, ― using "_", or "{}", or "nil" ― has been around since 2016 at least, because zero-initializing a struct is indeed way too verbose.

> Better still, start handling the error.

Most of the time, you can do this only two-three levels higher on the call stack, and even here it's most likely to be wrapping up of "return nil, fmt.Errorf("frobbing bar: %w", err)" sort.

And yes, I am too excited and scared of the generics. I'd like to see generic map- and channel-related algorithms and patterns, and sort.Interface is an ugly hack, even if an ingenious one.


> because zero-initializing a struct is indeed way too verbose

hmm. I don't have the problem you're describing, because I generally declare the return struct at the start of the function and then return that. E.g.

    func something() (mystruct,error){
        result := mystruct{}
        if badthing() then {
            return result, fmt.Errorf("bad thing happened: %w",err)
        }
        //do something to result before returning it
        return result
    }
I must admit I don't understand how the proposal works fully. Would `x==nil` return true for an int with value 0? Both answers to this seem wrong to me.


> hmm. I don't have the problem you're describing, because I generally declare the return struct at the start of the function and then return that.

The problem with this is that, in a larger function, I have to read all the way up to confirm if result is an empty value or something else. I think returning myStruct{} is much better, and having a way to easily return a default myStruct would be nice. Calling that "nil" seems to be a bad idea to me, as others are pointing out.

> I must admit I don't understand how the proposal works fully. Would `x==nil` return true for an int with value 0? Both answers to this seem wrong to me.

I think it would return false in that case. There is already precedent for this in Go, in fact, with arrays/slices. Even though slices themselves are value types (they wrap a pointer) they can be nil, but []someStruct{} == nil is false.


Yeah but that's because the zero value for slices is an empty slice, not a nil slice. This is what we should have more of.

If in the proposal (x==nil) == false then we get the ridiculous position where:

    var x int
    x=nil
    if x==nil {
        //this never happens despite having just set x=nil
    }
which seems ridiculous.

Though the other case, of returning true, also seems ridiculous, because nil and 0 are not the same thing at all.


The difference between an empty slice and a nil slice is one of the most inexplicable (to me) warts of the language, especially since it doesn't apply to anything else (maps can't be nil, nil and empty interfaces actually have different properties).

Anyway, I do agree that using `nil` as a base case for all types would introduce more confusion. What I think could be useful would be a `default` keyword for this:

  var x int
  x = default
  if x == default {
    this is true
  }
This would obey rules similar to `iota` in terms of its integration into the language.


Yeah that would work better than using nil. And it would make it clear that it was being initialised to whatever the default value for that type is.

Though, of course, most coders would leave out the second step here since the compiler initialises all variables with their type's default value ;)


Sure :), the really nice example would be

  return default, default, default, err


This simply would not compile because of "(mismatched types int and nil)". Solved already.

That's why I actually would prefer the "_" syntax:

    var x int
    x = _
    _ = 42

    if x == _ {
        // this happens even though you've assigned 42 to _, yes
    }


nil for non-pointer types should only be assignable, not comparable.


It doesn't solve the problem then, because for non-pointer types we have to have multiple zero values: one for assignment and one for comparison.


To be fair, it's not that much of a problem, and even then: "if result == _ { ... }" instead of "if result == MyPrettyStruct{} { ... }" is arguably more intent-revealing, as for "if number == _ { ... }", well, I guess the recommendation would be "just don't do that".

Perhaps gofmt could even automatically replace it?


> Most newbie gophers don't understand how powerful duck-typed interfaces are

Duck-typed interfaces are kind of broken in Go, though. I can use an Impl anywhere I can use an Abstract, but if I have a function that takes a slice of Abstract, I can't pass it a slice of Impl, because it's the wrong type.


Sorry I don't really understand your terminology?


    type Abstract interface {
        value() string
    }

    type Impl struct {
        str string
    }

    func (impl Impl) value() string {
        return impl.str
    }

    func ShowAbstracts(arr []Abstract) {
        for _, a := range arr {
            fmt.Println(a.value())
        }
    }

    func main() {
        arr := []Impl{Impl{"a"}, Impl{"b"}}
        ShowAbstracts(arr) // This doesn't work, because arr is []Impl, not []Abstract
    }

It works with `arr := []Abstract{Impl{"a"}, ...}` of course, but if you already have a 100K slice of Impl, you'd have to copy all 100K elements into a slice of Abstract to call ShowAbstracts in this example.


Thanks for the explanation :)

Slices are backed by arrays, not actual arrays - copying a 100K slice into another slice isn't as intensive as it sounds.

I'm not sure I see this as "interfaces are broken in Go", or something that requires generics to solve? (I do realise that the language itself uses generics to implement the copy keyword for this).


Unfortunately I think copying 100K elements is going to be exactly as painful as it sounds. The `[]Impl` is going to be (more or less) an array of 100K strings. The `[]Abstract` will be an array of interfaces, each of which is internally two pointers, so I'll have to allocate 1.6M of RAM and fill that RAM with pointers in order to convert a `[]Impl` into a `[]Abstract`.

Generics fix this handily, because I can write:

    func ShowAbstracts[T Abstract](arr []T) {
       for _, a := range arr {
            fmt.Println(a.value())
        }
    }
And now when I try to pass in my `[]Impl`, Go will compile a new `ShowAbstracts(arr []Impl)` transparently behind the scenes.


I wouldn't call it broken duck typing.

The problem is that ShowAbstracts could just modify the array and put an instance of Abstract there that _isn't_ an Impl, thus violating the original constraint of arr containing only instances of Impl.

Once again mutability/invariance raises its ugly head!


That is a very good point.


The extraordinarily repetitive error handling in Go drives me batty and I am thankfully not the only one who feels this way

Part One: Endless Error Handling https://jesseduffield.com/Gos-Shortcomings-1/

That blog also covers a lot of Go's wonderful defects advertised as 'features'.


Whereas it doesn't bother me at all. I've used Go for years now and it feels totally normal to check the error condition after every call. It feels strange switching to JS and there's just nothing - stuff can just error at any time and we're not going to do anything about it? That feels odd ;)


You can - one can put an exception handler at the place one truly wishes to handle the error as opposed to always introducing repetitive code at every single place in the call chain.

The latter tends to make the eyes glaze over in tedium after some time and due to the signal/noise ratio one misses real bugs in the error handling.


>You can - one can put an exception handler at the place one truly wishes to handle the error as opposed to always introducing repetitive code at every single place in the call chain.

I don't understand this point but I hear it raised often. At best I'm lukewarm on Go's error handling. Having if err != nil feels the same to me ergonomically as a try / catch.

Am I missing something by thinking that?


For a lot of complicated programming you can be 20 levels deep in function/method calls and most of the time you deeply don't care and you want to crash out to the main loop which will then either decorate the error for the user and crash itself or clean things up and loop if it can.

Then there's exceptional exceptions which should be not crash near the "edge" of the program and need to be caught and something done with, so you add those to the program.

What you wind up with using that approach is only exception handling being written where you need it, and the vast bulk of the code is understood to always crash and get handled in some inner loop, or even some external supervisor.

Go feels like it was a reaction to (probably early-2000s) Java programming patterns where at every level Java programs were catching and decorating and rethrowing their own exceptions. Neither of those approaches really are helpful though because you litter your code with all kinds of exception handling that nobody really cares that much about, and under both systems you can crash instead of retrying and all the useless error handling code you've written everywhere doesn't help you see where you've made that error. The 2010 erlang crash-first programming mentality works way better imo rather than error checking everywhere.

And Go out of the box makes it difficult to get stack traces, so you should really use an error library at least.


The number of JS scripts I've had to add any error handling to is too damn high.

I like that Go forces at least some kind of error handling.

The "signal/noise ratio" problem fades after a while - you start seeing "if err != nil {...}" as a single statement after a while, a steady rhythm in the code. You notice when it's not there, or when it's doing something different.



> Universal Nil: no. we need less nil not more. Nil-safe types would be a better answer.

Everyone needs optional values, due to Go's poor design the closest to this is to use nil.


> I'm kinda looking forward to generics, but also dreading it. Most newbie gophers don't understand how powerful duck-typed interfaces are and what they can do with them, and I have a feeling generics will be used for all sorts of situations that could be better handled with interfaces.

That really is the scariest part about it. I really hope the Go community will take this issue to hearth and push back against codebases with unnecessary generics. Otherwise the whole ecosystem will suffer from it.


I strongly agree with this. I'll also note that "enums" solves for nil (universal or otherwise).

> I have a feeling generics will be used for all sorts of situations that could be better handled with interfaces.

Very much agree. I'll also extend "interfaces" to include closures.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: