3 ms·
> e.g. net.OpError I have my own complain against Rust's error handlings, but I can't bring myself to praise what Go has been doing. One problem with the erro
by nirui 9mo ago
> e.g. net.OpError
I have my own complain against Rust's error handlings, but I can't bring myself to praise what Go has been doing.
One problem with the error interface is that you can't know exactly which error type will be returned, and the Go authors may add new error types form time to time. `net.OpError` itself is a great example for that, which is a newer type than net.Error.
This style of error signalling is best used when the error handling is binary, either "No error, continue" or "Errored out, abort", but not branched out path like "If encountered error A, do this. If encountered error B, do that. Otherwise, abort".
Rust's error handling encourages such branched out handlings by default, but if you want to so the same in Go, there will be a nightmarish manual type discovery and tracking operation waiting for you.
So for me, I rather use a dedicated signal code to tell which error branch I should do next rather than relying on the error, i.e:
if next, err := func(); err != nil { // If errored, abort it
return err
} else if next == condition1 {
return op1()
} else if next == condition2 {
return op2()
} else {
panic("unsupported condition")
}
- iamcalledrob 9mo ago> This style of error signalling is best used when the error handling is binary, > either "No error, continue" or "Errored out, abort", but not branched > out path like "If encountered error A, do this. If encountered error B, > do that. Otherwise, abort". If I'm understanding you correctly, you can implement your example in a fully idiomatic way, and the stdlib does this in a bunch of places. Errors are just values, so you can return whatever best suits the use-case. The gamut of expected errors might not be part of type signature, but it can be part of your documentation. The "signal code" you refer to could be implemented via: 1. Returning error structs (e.g. ErrFoo, ErrBar) and using `errors.As()` access those structs -- useful if you have a highly structured error with additional fields. type ErrFoo struct { // fields // } type ErrBar struct { // fields // } if var errFoo *ErrFoo; errors.As(err, &errFoo) { // handle ErrFoo } if var errBar *ErrBar; errors.As(err, &errBar) { // handle ErrBar } 2. Returning a single error struct (MyErr) with an `Op` field, as `net.OpError` does. Similarly accessing via `errors.As()` // top-level, e.g. exported by package type MyErrorStruct struct { Op string Path string } var ( OpFoo string = "foo" OpBar string = "bar" ) var opErr *MyErrorStruct if errors.As(err, &opErr) { if opErr.Op = OpFoo { // handle OpFoo at path opErr.Path } if opErr.Op = OpBar { // handle OpBar at path opErr.Path } } 3. Returning predefined errors, e.g. `var ErrFoo = errors.New("foo")` and checking for them via `errors.Is()` // top-level, e.g. exported by package var ErrFoo = errors.New("foo") var ErrBar = errors.New("bar") if errors.Is(err, ErrFoo) { // handle ErrFoo } if errors.Is(err, ErrBar) { // handle ErrBar } Sidenote: I feel like people struggle to internalise that Go errors are just values* and are therefore are wildly flexible, yet not special in any way. I wonder if this is because other languages treat errors as something special?
- yencabulator 9mo ago> Sidenote: I feel like people struggle to internalise that Go errors are just values* and are therefore are wildly flexible, yet not special in any way. I think it's more they haven't used Go interfaces much. I think most programming languages these days have errors-are-values, e.g. Rust Result transports a normal value, Python & Javascript exceptions are just values even when they are carried over a wildly different control flow. (The argument for sum types and exhaustiveness is valid, but most languages don't have those so focusing that critique on Go is misguided. They are nice in Rust.)
- nirui 9mo ago> If I'm understanding you correctly, you can implement your example in a fully idiomatic way, and the stdlib does this in a bunch of places. I don't think you understood my point. Instead, I believe you launched your argument too quickly. Because if you continue reading, you'll see: > ... but if you want to so the same in Go, there will be a nightmarish manual type discovery and tracking operation waiting for you. Which should deter you from providing the example where you declared the `ErrFoo` and `ErrBar` type, since it's basically the "nightmarish manual type discovery" scenario unfolding in basically realtime. But let's continue with the example to make my argument more full: Let's say one day the `ErrFoo` and `ErrBar` type is not sufficient, so you declares another type `Err2000`. Now what happens down stream? Since the some function may just return an error interface, there's no way for the end user to know you've added that error type. From their prospective, everything is unchanged, and still compiles just fine. If the user's code is relying on the exact error type for branching, then they have to perform "manual type discovery and tracking" to monitor the changes to your code (and everything above it, really) to ensure that the branching is still done correctly. In Rust however, since error is a emun, all error conditions must be handled or explicitly ignored during a `match`, if you write something like match File::open("filename.txt") { Ok(file) => {} Err(io_err) => match io_err.kind() { ErrorKind::IsADirectory => {} // Notice how not every error type is handled }, } The compiler will just stand up and make your ass weep. Meaning if you add some new error conditions, your user will know that for sure when they compile. (BTW, `std::io::ErrorKind` is indeed `non_exhaustive`, but you the maker of the error type still have to explicitly/intentionally declare it to make it non_exhaustive) So, let's read my comment again: > Rust's error handling encourages such branched out handlings by default, but if you want to so the same in Go, there will be a nightmarish manual type discovery and tracking operation waiting for you. (Also, "by default" is there to indicate that yeah you can boxing a `std::error:Error` trait in Rust, then you ended up with something like how Go handles error. I know.)