DEV Community

林子篆
林子篆

Posted on • Originally published at dannypsnl.github.io on

Error is Value

I think most of 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. But 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")
Enter fullscreen mode Exit fullscreen mode

Can we? Ah…, No!

Correct code is

conn, err := net.Dial("tcp", "server:6666")
if err != nil {
    panic(err)
}
Enter fullscreen mode Exit fullscreen mode

Then we writing something to the connection.

nBtye := conn.Write([]byte{`command`})
Enter fullscreen mode Exit fullscreen mode

We want that, but the real code is

nBtye, err := conn.Write([]byte{`command`})
if err != nil {
    panic(err)
}
// using nByte
Enter fullscreen mode Exit fullscreen mode

Next, we read something from server:6666, so we create a reader.

reader := bufio.NewReader(conn)
response := reader.ReadString('\n')
Enter fullscreen mode Exit fullscreen mode

No! We have to handle the error.

response, err := reader.ReadString('\n')
if err != nil {
    panic(err)
}
// using response
Enter fullscreen mode Exit fullscreen mode

But 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.Dail(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
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

But 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
}
Enter fullscreen mode Exit fullscreen mode

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`})
})
Enter fullscreen mode Exit fullscreen mode

Wait! But 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)
    }
}
Enter fullscreen mode Exit fullscreen mode

Now, we coding like

w := NewErrorWrapper("tcp", "server:6666")

w.Then(func(network, host string) (net.Conn, error) {
    conn, err := net.Dail(network, host)
    return conn, err
}).Then(func(conn net.Conn) error {
    _, err := conn.Write([]byte{`command`})
    return err
}).Final(func(e error) {
    panic(e)
})
Enter fullscreen mode Exit fullscreen mode

Top comments (2)

Collapse
 
dannypsnl profile image
林子篆

Maybe I should write something about reflect?

Collapse
 
dannypsnl profile image
林子篆

I made some explain at error wrapper gist