Error is Value
I think most Gopher had read
error-handling-and-go.
Has anyone had watched Go
Lift? Let's getting start from Go Lift! The point of Go Lift is:
Error is Value. Of course, we know this fact. Do you really understand
what that means? In Go Lift, John Cinnamond mentions a trick about
wrapping the error by command executor. For example, we create a
connection to server:6666
by TCP.
conn := net.Dial("tcp", "server:6666")
Can we? Ah…, No! The correct code is:
conn, err := net.Dial("tcp", "server:6666") if err != nil { panic(err) }
Then we write something through the connection.
nBtye := conn.Write([]byte{`command`})
We want that, but the real code is:
nBtye, err := conn.Write([]byte{`command`}) if err != nil { panic(err) } // using nByte
Next, we read something from server:6666
, so we create a reader.
reader := bufio.NewReader(conn) response := reader.ReadString('\n')
No! We have to handle the error.
response, err := reader.ReadString('\n') if err != nil { panic(err) } // using response
Howver, the thing hasn't ended yet if we have to rewrite the command if response tells us the command fail? If we are working for a server, we can't just panic? So Go Lift has a solution:
func newSafeConn(network, host string) *safeConn { conn, err := net.Dial(network, host) return &safeConn{ err: err, conn: conn, // It's fine even conn is nil } } type safeConn struct { err error conn net.Conn } func (conn *safeConn) Write(bs []byte) { if conn.err != nil { // if contains error, do nothing return } _, err := conn.Write(bs) conn.err = err // update error } func (conn *safeConn) ReadString(delim byte) string { if conn.err != nil { return "" } reader := bufio.NewReader(conn.conn) response, err := reader.ReadString("\n") conn.err = err return response }
Then usage will become
conn := newSafeConn("tcp", "server:6666") conn.Write([]byte{`command`}) response := conn.ReadString('\n') if conn.err != nil { panic(conn.err) } // else, do following logic
Can we do much more than this? Yes! We can have an error wrapper for executing the task.
type ErrorWrapper struct { err error } func (wrapper *ErrorWrapper) Then(task func() error) *ErrorWrapper { if wrapper.err == nil { wrapper.err = task() } return wrapper }
Then you can put anything you want into it.
w := &ErrorWrapper{err: nil} var conn net.Conn w.Then(func() error { conn, err := net.Dial("tcp", "server:6666") return err }).Then(func() error { _, err := conn.Write([]byte{`command`}) })
Wait! We need to send the connection to next task without an outer scope
variable. But how to? Now let's get into reflect
magic.
type ErrorWrapper struct { err error prevReturns []reflect.Value } func NewErrorWrapper(vs ...interface{}) *ErrorWrapper { args := make([]reflect.Value, 0) for _, v := range vs { args = append(args, reflect.ValueOf(v)) } return &ErrorWrapper{ err: nil, prevReturns: args, } } func (w *ErrorWrapper) Then(task interface{}) *ErrorWrapper { rTask := reflect.TypeOf(task) if rTask.NumOut() < 1 { panic("at least return error at the end") } if w.err == nil { lenOfReturn := rTask.NumOut() vTask := reflect.ValueOf(task) res := vTask.Call(w.prevReturns) if res[lenOfReturn-1].Interface() != nil { w.err = res[lenOfReturn-1].Interface().(error) } w.prevReturns = res[:lenOfReturn-1] } return w } func (w *ErrorWrapper) Final(catch func(error)) { if w.err != nil { catch(w.err) } }
Now, we're coding like:
w := NewErrorWrapper("tcp", "server:6666") w.Then(func(network, host string) (net.Conn, error) { conn, err := net.Dial(network, host) return conn, err }).Then(func(conn net.Conn) error { _, err := conn.Write([]byte{`command`}) return err }).Final(func(e error) { panic(e) })