Hacker News new | ask | show | jobs
by ktt 4703 days ago
Interesting that this snippet:

  func (g *Gopher) DumpBinary(w io.Writer) error {
    err := binary.Write(w, binary.LittleEndian, int32(len(g.Name)))
    if err != nil {
        return err
    }
    _, err = w.Write([]byte(g.Name))
    if err != nil {
        return err
    }
    err = binary.Write(w, binary.LittleEndian, g.Age)
    if err != nil {
        return err
    }
    return binary.Write(w, binary.LittleEndian, g.FurColor)
  }
could be written like this:

  func (g *Gopher) DumpBinary(w io.Writer) {
    binary.Write(w, binary.LittleEndian, int32(len(g.Name)))
    w.Write([]byte(g.Name))
    binary.Write(w, binary.LittleEndian, g.Age)
    binary.Write(w, binary.LittleEndian, g.FurColor)
  }
if the language supported exceptions.
6 comments

Go supports exceptions (called "panics").

The substantive difference between Go and, e.g., Java with regard to exceptions is that Go builtin and standard library functions panic in a much narrower range of circumstances than Java's standard library. Go seems to prefer that the decision that an error condition is treated as a panic is generally left to user code that is written with more awareness of what is exceptional in the context of the role of that code than standard library code has.

So, you could write the code in pretty much exactly the way you propose in Go; you'd just need to write a wrapper function around binary.Write that panics on errors.

Go seems to prefer that the decision that an error condition is treated as a panic is generally left to user code that is written with more awareness of what is exceptional in the context of the role of that code than standard library code has.

That makes little sense in the context of ioWriters and, frankly, most contexts.

How often do you write code where you deliberately want to be oblivious of IO errors?

> How often do you write code where you deliberately want to be oblivious of IO errors?

The existence of error returns means that the IO library function "not panicking" and the calling code being "oblivious of IO errors" are not equivalent.

I think the motivation for the Go convention of keeping panics internal and reducing to error returns in library APIs minimizing the potential downsides of the way unchecked exceptions are not part of the declared interface of functions and yet have a major effect on control flow. A convention of using panics (which amount to unchecked exceptions) only within logically bounded units is, IMO, a sensible approach to this.

(You could do this with checked exceptions, which require additional syntax in declarations. When you already have support multiple valued returns, I don't see that checked exceptions get you much that's worth making signatures more complicated.)

When you already have support multiple valued returns, I don't see that checked exceptions get you much that's worth making signatures more complicated.

Seriously?

Does your test-suite exercise every potential I/O error and timeout on every single of your I/O calls?

/pedant hat on

Technically, the language does support exceptions. That said, they're in the "please never use this, ever."

/pedant hat off

The spirit of your comment is right, however -- the wonky code resulting from error handling, just like the "compile error on unused vars or imports," is something most new Go users find jarring.

> Technically, the language does support exceptions. That said, they're in the "please never use this, ever."

No, its not. The convention is that any use of panics within libraries should be internal, and that libraries' exposed interfaces should use error returns. [1] Use of panics internal to libraries, or use of panics within application code that is not creating a library for others to consume, is not discouraged.

[1] http://blog.golang.org/defer-panic-and-recover

Thanks for the clarification, and the link :D
If the language supported exceptions, how would you write this func?

  func (g *Gopher) DumpBinary(w io.Writer) {
    // Ignore all errors
    _ = binary.Write(w, binary.LittleEndian, int32(len(g.Name)))
    _, _ = w.Write([]byte(g.Name))
    _ = binary.Write(w, binary.LittleEndian, g.Age)
    _ = binary.Write(w, binary.LittleEndian, g.FurColor)
  }
The language supports exceptions (panics). You could do something like (this is untested code):

  func panicBinaryWrite(w io.Writer, b binary.ByteOrder, data interface{}) {
    if err := binary.Write(w, b, data); err != nil {
      panic("Error in binary.Write")
    }
    return
  }

  func panicWrite(w io.Writer, data interface{}) {
    if _,err := w.Write(binary.LittleEndian,data); err != nil {
      panic("Error in io.Writer#Write")
    }
    return 
  }

  func ignoreErrors(f func()) {
    defer func() { 
      _ = recover()
    }()
    f()
    return
  }

  func (g *Gopher) DumpBinary(w io.Writer) {
    // Ignore all errors
    ignoreErrors(panicBinaryWrite(w, binary.LittleEndian, int32(len(g.Name))))
    ignoreErrors(panicWrite([]byte(g.Name)))
    ignoreErrors(panicBinaryWrite(w, binary.LittleEndian, g.Age))
    ignoreErrors(binary.Write(w, binary.LittleEndian, g.FurColor))
  }
It's trivial to write a function to ignore exceptions if that's what you want.

    def ignoreExceptions[A](a: => A): Unit = try {a} catch {case _ =>}

    def dumpBinary(g: Gopher, w: Writer) = {
      ignoreExceptions binary.Write(w, binary.LittleEndian,     int32(len(g.Name)))
      ignoreExceptions w.Write([]byte(g.Name))
      ignoreExceptions binary.Write(w, binary.LittleEndian, g.Age)
      ignoreExceptions binary.Write(w, binary.LittleEndian, g.FurColor)
    }
Though honestly I think a better solution is monads.

    //returns Validation - either success, or the first error (which stops processing)
    //return values are directly accessible, because later code won't run unless earlier code succeeds
    for {
      _ <- binary.Write(w, binary.LittleEndian, int32(len(g.Name)))
      _ <- w.Write([]byte(g.Name))
      _ <- binary.Write(w, binary.LittleEndian, g.Age)
      _ <- binary.Write(w, binary.LittleEndian, g.FurColor)
    } yield {}
    //Runs all the operations, ignoring errors
    //type system will force you to check a return value before you can use it
    binary.Write(w, binary.LittleEndian, int32(len(g.Name)))
    w.Write([]byte(g.Name))
    binary.Write(w, binary.LittleEndian, g.Age)
    binary.Write(w, binary.LittleEndian, g.FurColor)
Laziness and irresponsibility aside, why would you be writing code without even a minimal level of error checking?
It's just a different (arguably better default) to have to explicitly ignore errors and/or explicitly bubble them up the call chain.

You'll always be in control of your code's control flow that way. You'll never have some random library 5 levels beneath your code throw an exception that you didn't know about, causing your function to return prematurely, resulting in your function accidentally leaving some file handle open, a mutex locked or some similar problem (I realize in Go this should be handled via defer anyway, so would probably not be an issue in practice). In short, you know exactly what error cases you should be thinking about and are forced to explicitly reason about whether or not you care about it.

A few languages fix some of these concerns with checked exceptions, but checked exceptions have their own limitations and drawbacks. Of course, regardless of the approach taken, lazy programmers will always do the minimal amount of effort required to ignore errors/exceptions.

What are the problems with checked exceptions that don't apply to go's approach?
I have general issues exceptions. I've seen too many developers use exceptions in place of conditionals, and in the worst abuses, use exceptions as some hacked form of GOTO that lets them jump to different points of execution within their call stack.

They're just a little too easy to abuse and are often used for non-exceptional cases. So while checked exceptions improve on exceptions, they are still exceptions at the end of the day.

Checked exceptions are harder to ignore :-)

    result, _ = someFunc()
Huh?

   try {
      result = someFunc()
   } catch {}
They do something interesting in the 'template' lib, they have a func called Must which will eat the error and throw a panic if you don't want to handle each error yourself, I like it because it's much more explicit than the func just throwing the exception without you knowing.

http://golang.org/src/pkg/text/template/helper.go?s=576:619#...

  func must(err error) {
    if err != nil { panic(err) }
  }  

  func (g *Gopher) DumpBinary(w io.Writer) (err error) {
    defer func() {
      err, _ = recover().(error)
    }
    must(binary.Write(w, binary.LittleEndian, int32(len(g.Name))))
    must(w.Write([]byte(g.Name)))
    must(binary.Write(w, binary.LittleEndian, g.Age))
    must(binary.Write(w, binary.LittleEndian, g.FurColor))
    return
  }
That won't work as it is with `w.Write`, as it returns two values, but yes, the idea is perfectly valid.