Hacker News new | past | comments | ask | show | jobs | submit login

If only there were generics, and no nulls, oh and strongly typed errors... regardless, this is Very impressive.



What about the way Go handles errors today makes them not "strongly typed"?


> What about the way Go handles errors today makes them not "strongly typed"?

Error as type or error as values ? the std lib promotes error as values (i.e. check equality) instead of errors as type (i.e. check the type). Go error system WAS written with errors as value in mind. There is no point having errors in place of exceptions if errors were intended to be used as types (which they are not, as said previoulsy). Basically developers are implementing their own mediocre exception system on top of Go errors.

The error as value thing made sense in C 30 years ago, it doesn't in a language created less than 10 years ago.

There are a lot of C inspired patterns in Go that make the language half modern/ half dated in strange ways. That's fine when one comes from C though, that isn't when one comes from anything remotely modern. But I guess it's why Go is successful, it's basically C with garbage collection.


error is an interface type is Go, which means an error contains both a type and a value, or "error as type" and "error as value" are both true in Go.


I said that already. And that's not the problem at end. When you test an error, do you test against a value or a type in order to know what kind of error it is ?


You can test the interface. A type is just an interface around memory, albeit more consrained.


> You can test the interface. A type is just an interface around memory, albeit more consrained.

Wow, again, that's not the problem here. Errors in the standard libraries are defined at values. There is no point testing it as interfaces, it will not give you the nature of the error, since they are all defined with fmt.Errorf . Do you understand now the problem ? the problem is being consistant across codebases between errors are values and errors as types.


That's because single error instances are much cheaper than always creating a new instance of a given type. No need to create garbage for lots of common errors. Of course you could have a dedicated type plus a singleton implementation of it in Go. But what would be the advantage? Checking if err.(type) == io.EofType does not give you more information the only checking if err == io.Eof, as long as you don't store any error instance related information in it. Which makes sense for custom errors and which Go absolutely allows you to do.


> they are all defined with fmt.Errorf

No, an error implements the error interface. It means that it can be a value of any type that implements the constraint of having an Error method.


> No, an error implements the error interface. It means that it can be a value of any type that implements the constraint of having an Error method.

It doesn't matter what a value implements if you don't test its content by type. Std lib errors are meant to be tested by value, not by type. It has nothing to do with interfaces again. When you get a std lib error in order to know what error it is you compare it to an exported value, not its type. I don't know why you keep on insisting on interfaces that's not the core of the issue here.


We do have the same problem in Java and .NET though.

I have already used libraries that catch the exceptions internally and return some kind of error value instead. :(


Neither the Java std lib or the .NET std lib do that though, they don't declare an exception as a static constant you need to compare with. Because it's (rightfully so) considered bad coding. Exceptions give you some valuable informations like stack traces. Go errors gives you nothing. They are inferior to Exceptions and a relic of C programming patterns.


The compiler provides you no help at all with them, and no syntax that makes error conditions and handling separate. It also mixes application logic and recovery logic.

Basically everything that's problematic with returning a status int in C, but all new, hip, and backed up by Rob Pike's pseudointellectual bullshit and a bunch of Silicon Valley 20somethings.

They could at least, you know, have an Either type or something. Anything?


>It also mixes application logic and recovery logic.

When did this separation become law ? What if the "application logic" requires recovery ?

>They could at least, you know, have an Either type or something

(int64, error) in func ParseInt() (int64, error) is your Either type. And checking if you got the "left or the right side of the Either" is IMHO much shorter and clearer than in Scala.

https://golang.org/pkg/strconv/#ParseInt

http://www.scala-lang.org/api/rc2/scala/Either.html

>backed up by Rob Pike's pseudointellectual bullshit and a bunch of Silicon Valley 20somethings

Why the ad hominems ?


cringe.

How ecactly is having to check twice the amount of cases an improvement (note btw, that checking for Left/Right is doing it wrong)?


You only have to check 1 case in Go (`if err != nil { ... }`). What language lets you check half a case?


  (int64, error)
gives you exactly four possibilities.

Either gives you exactly the two you want.


Please feel free to enumerate them.


  (value, no error)
  (value, error)
  (no value, error)
  (no value, no error)


The convention is that if the err == nil then the value is not nil. The exceptions to this rule are very few and usually specified in the documentation. Normally you only have to check for error.


The fact that it's a convention, and not a compiler error is the entire issue at debate here.


There is no "no value" representation for int64. The only two cases are "<int64>, nil" or "<int64>, <error>".


Yes, but at least they are making the tools we may eventually have to rely on due to market pressure (Docker, K8s, ...) in Go instead of C.


Don't forget Brian Kernighan and Ken Thompson!


Typed vs untyped nil means that, in practice, all functions must return an error of type 'error', and force clients to downcast at runtime:

https://golang.org/doc/faq#nil_error

Of course, there are ways around this such as returning a struct, but then that's no longer compatible with the error interface.


> Of course, there are ways around this such as returning a struct, but then that's no longer compatible with the error interface.

Which your struct can easily fulfill. That's the beauty of Go interfaces.


The linked FAQ specifically talks about returning pointers to structs that fulfil the 'error' interface and why it's a bad idea.


It's not that it's a bad idea, just that because of the "nil interface" absurdity it can happen if you accidentally mix concretely typed variables and interfaces, as in the example.

This is perfectly valid and doesn't cause the nil issue:

    return someErrorStruct{}
...where someErrorStruct is a strict that implements the "error" interface. Using structs for errors is fine, and is in fact generally preferable to singletons like io.EOF, which can (by their very nature) never be extended with more data about the error.


> absurdity

There's nothing absurd about it; interfaces are a reference type. If you have a reference to a reference, then checking the "outer" reference for nil doesn't tell you anything about the nullity of the "inner" reference. The advice is just a special case of "don't needlessly use pointers-to-pointers".


Go chose to rely heavily on nil pointers, which is a design mistake (see Tony Hoare's apology for inventing it). The resultant tension between interfaces and nils is, in my opinion, an absurd side effect that cannot be explained away as anything except an ugly wart. We should have something better than this in 2016.

I say this as someone who uses Go daily for my work and mostly likes it (despite the many warts).


I don't especially love nil either, but people make too big a deal of it. The only arguments against it are hypothetical scenarios, anecdotal examples, and appeals to Hoare's authority. While there's probably a more ergonomic design, there's no substantial evidence that nil is the catastrophe it's made out to be. Using terms like "absurdity" and "catastrophe" seems overly dramatic without some decent evidence.


I don't think I'm being overly dramatic, actually. I deal with nil oversights on a daily basis during development, and I would say that it is is the main source of unexpected crashes in anything. It equally applies to other languages such as Ruby and Python.

It's exacerbated by the fact that Go chose to make things like maps, slices and channels pointers, too. It has an excellent philosophy about zero values (so you can't get nil strings, even though they are pointers internally), yet goofed when it came to something as intuitive as "var foo []string", and claimed this behaviour to be a feature. The (nil, nil) interface is just icing on a crappy cake.

The fact that such a new language as Go doesn't have a way to express missing values safely should be disappointing to developers.


By a wide margin, the biggest production issues I see are index out of bounds or key errors (regardless of language). When I'm treating `nil` as a valid state for some object, I take extra care to test its permutations, and uninitialized maps/interfaces/etc are quickly discovered during testing (every test has to initialize, so this logic is well-covered).

> The (nil, nil) interface is just icing on a crappy cake.

The same problem exists with languages without nil. For example, if you choose to do the stupid thing and return Option<Option<error>> when you only need Option<error>, then checkout the outer Option<> for None is not going to magically guarantee that the inner Option<> is not None.

> It has an excellent philosophy about zero values (so you can't get nil strings, even though they are pointers internally), yet goofed when it came to something as intuitive as "var foo []string", and claimed this behaviour to be a feature.

What are you talking about? nil slices are every bit as safe as an empty slice or an empty string (which is just an immutable empty slice).

> The fact that such a new language as Go doesn't have a way to express missing values safely should be disappointing to developers.

I agree, but I'm mildly annoyed by it, but as it's the least of all of my problems, I find words like "absurdity" to be too heavy-handed.


Nil slices do cause problems. One when it's an aliased type that satisfies an interface (again with the nil interfaces!). Another is that it leads to inconsistencies: json.Marshal(([]string)nil) returns "null", for example, not "[]". Yet another annoyance caused by nils (including nil slice) is that reflect.Value becomes a lot more byzantine than it ought to have been, requiring careful use of IsValid(), Kind checks etc. when you want to deal with something that is either an interface or something else.

As for Option: Not sure how that's an argument. And anyway, a language with real sum types will never allow going down a branch that doesn't receive a correctly matched value.


> One when it's an aliased type that satisfies an interface (again with the nil interfaces!)

It sounds like you're again confusing nil interfaces with an interface holding a nil value (in particular, there is no way to get a nil interface from a nil slice). Here's an example that demonstrates nil slices do not cause problems: https://play.golang.org/p/tSA_otqg3-

> Another is that it leads to inconsistencies: json.Marshal(([]string)nil) returns "null", for example, not "[]".

1. This is unrelated to the language; it's the behavior implemented by the JSON library

2. This behavior is correct; a nil slice is not the same as an empty slice:

        fmt.Println([]int(nil) == nil) // true
        fmt.Println([]int{} == nil)    // false
3. This behavior is consistent: https://play.golang.org/p/NjdO0boHln

> As for Option: Not sure how that's an argument.

You posited that Go's nils are bad because you can satisfy an error interface with a pointer type, and then when you return (*ConcreteType)(nil), your error handling code executes. The problem here is unrelated to nil or to interfaces; the problem is that you're nesting one binary type inside another (binary in the literal sense of having two states, either nil or not nil in this case). You would have the same problem in Rust had you done Result<Foo, Option<ConcreteError>> or (in the case of a single return value) Option<Option<ConcreteError>>. You would fix the problem in either languages by recognizing that you only have 2 states (error or not error) and removing the nesting (in Rust: Result<Foo, Error> or Option<Error>; in Go `return ConcreteType{}`).

> And anyway, a language with real sum types will never allow going down a branch that doesn't receive a correctly matched value.

I agree, and it would be nice if Go had this, but this is also not a very significant problem--this problem is blown way out of proportion.


The return type would still be the error interface. If you want more information than the error interface you can just use a type switch/assertion.




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

Search: