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)
})