Go method chaining and error handling
Asked Answered
F

6

25

I want to create a method chaining API in Go. In all examples I can find the chained operations seem always to succeed which I can't guarantee. I therefore try to extend these to add the error return value.

If I do it like this

package main

import "fmt"

type Chain struct {
}

func (v *Chain)funA() (*Chain, error ) {
    fmt.Println("A")
    return v, nil
}
func (v *Chain)funB() (*Chain, error) {
    fmt.Println("B")
    return v, nil
}
func (v *Chain)funC() (*Chain, error) {
    fmt.Println("C")
    return v, nil
}

func main() {
    fmt.Println("Hello, playground")
    c := Chain{}
    d, err := c.funA().funB().funC() // line 24
}

The compiler tells me chain-err-test.go:24: multiple-value c.funA() in single-value context and won't compile. Is there a good way so funcA, funcB and funcC can report an error and stop that chain?

Fineman answered 4/12, 2014 at 15:26 Comment(7)
You can use panic but of course this means you'll have to recover on each method or at the root of it. You can also make the Chain object stateful with an error and check it for each method.Jemina
@Jemina true, but I'm wondering whether there's a good idiomatic way for doing this. In my world error conditions are everywhere and in some places (i.e. operators as in golangpatterns.info/object-oriented/operators ) such chaining should give a nice API. (the first example on that page i.e. isn't a nice API - either you have tons of functions for all types or a switch where wrong types aren't detected by the compiler)Fineman
I remember a nicer API involving functions returning functions, as in dave.cheney.net/2014/10/17/functional-options-for-friendly-apis (done after commandcenter.blogspot.com.au/2014/01/…). It is not exactly "chaining" though.Illdefined
@Fineman I did something similar but used channels to chain output streams, and an error just closes the channel, winding down the entire chain. It's probably not what you need though.Jemina
I know this isn't the answer you're looking for, but the idiomatic thing is to avoid chaining. There's nothing wrong with chaining, but it's a consequence of the idiom of using error values instead of exceptions.Lesleylesli
@Lesleylesli well if there is no good way then your answer below might be a good one, my hope was that I've overseen a thing being quite unexperienced in Go.Fineman
@Fineman I think that's what you'll find. Go places language simplicity very highly, and "clever" workarounds to impose other idioms are often difficult, even when the result is less boilerplate (and rightly so; idiomatic boilerplate is still much more readable than a "clever", non-idiomatic solution). Again, this doesn't mean method chaining is bad practice--it just isn't well suited to Go.Lesleylesli
L
43

Is there a good way so funcA, funcB and funcC can report an error and stop that chain?

Unfortunately, no, there is no good solution to your problem. Workarounds are sufficiently complex (adding in error channels, etc) that the cost exceeds the gain.

Method chaining isn't an idiom in Go (at least not for methods that can possibly error). This isn't because there is anything particularly wrong with method chains, but a consequence of the idiom of returning errors instead of panicking. The other answers are workarounds, but none are idiomatic.

Can I ask, is it not idiomatic to chain methods in Go because of the consequence of returning error as we do in Go, or is it more generally a consequence of having multiple method returns?

Good question, but it's not because Go supports multiple returns. Python supports multiple returns, and Java can too via a Tuple<T1, T2> class; method chains are common in both languages. The reason these languages can get away with it is because they idiomatically communicate errors via exceptions. Exceptions stop the method chain immediately and jump to the relevant exception handler. This is the behavior the Go developers were specifically trying to avoid by choosing to return errors instead.

Lesleylesli answered 4/12, 2014 at 17:35 Comment(5)
Nice answer (+1). Can I ask, is it not idiomatic to chain methods in Go because of the consequence of returning error as we do in Go, or is it more generally a consequence of having multiple method returns?Malonis
Thanks for this; it's a good clarification. If only I could upvote twice ;)Malonis
To add to your answer to @user1503949's question - what if you could have multiple receivers for a method in Go? You could then include errors in method chains, right? If that's the case, I'd argue it's the lack of this feature that is the reason chaining isn't idiomatic. Including errors in a hypothetical multiple-receiver Go would require a succeeding method to handle the error of the preceding method, which in my opinion, for chains in Go, just makes sense. (Edit: that said I'm sure this would break the language in other ways)Highup
@EricDubé I don't think multi-receivers makes sense; otherwise you'll be calling functions like this: (foo, nil).Bar() just to get method chaining. The real solution to this problem (insofar as it's a problem) is monads. Define a short-circuit-on-error-monad and chain like so: foo >>= bar >>= baz (where bar takes a FooResult, and baz takes a BarResult), but this requires a more sophisticated type system for which there are pros and cons (notably a steeper learning curve and less-consistency across programs, not to mention a better-optimizing compiler to maintain performance).Lesleylesli
What's wrong with (foo, nil)? Or, rather, (foo, error(nil))? You'd only have to do it preceding the first method in the chain, which IMO would be a good thing since it makes it clear that chaining is happening.Highup
A
14

You can try like that: https://play.golang.org/p/dVn_DGWt1p_H

package main

import (
    "errors"
    "fmt"
)

type Chain struct {
    err error
}

func (v *Chain) funA() *Chain {
    if v.err != nil {
        return v
    }
    fmt.Println("A")
    return v
}
func (v *Chain) funB() *Chain {
    if v.err != nil {
        return v
    }
    v.err = errors.New("error at funB")
    fmt.Println("B")
    return v
}
func (v *Chain) funC() *Chain {
    if v.err != nil {
        return v
    }
    fmt.Println("C")
    return v
}

func main() {
    c := Chain{}
    d := c.funA().funB().funC() 
    fmt.Println(d.err)
}
Archivist answered 30/5, 2018 at 9:47 Comment(0)
M
3

If you have control over the code and the function signature is identical you can write something like:

func ChainCall(fns ...func() (*Chain, error)) (err error) {
    for _, fn := range fns {
        if _, err = fn(); err != nil {
            break
        }
    }
    return
}

playground

Marinamarinade answered 4/12, 2014 at 16:5 Comment(3)
Why bother returning the original *Chain object? You're not doing anything with it. Even still, this is a clever solution, but it's the kind of clever that is less readable than the writing if err := funX(); err != nil { /* do something */ } for X={A, B, C} respectively.Lesleylesli
@Lesleylesli Mainly because I was too lazy to rewrite the code and wanted to show an example that'd work with his current code ;)Marinamarinade
Actually that's brilliant! *Chain could be a reference to some object that's being manipulated, forcing all the functions to always operate on the same thing. Where this falls short is that it's harder to give each function different parameters. Passing a bunch of anonymous functions that call the real functions could work, but it would be incredibly verbose I think.Highup
V
1

You can make your chain lazy by collecting a slice of funtions

package main

import (
    "fmt"
)

type (
    chainFunc func() error
    funcsChain struct {
        funcs []chainFunc
    }
)

func Chain() funcsChain {
    return funcsChain{}
}

func (chain funcsChain) Say(s string) funcsChain {
    f := func() error {
        fmt.Println(s)

        return nil
    }

    return funcsChain{append(chain.funcs, f)}
}


func (chain funcsChain) TryToSay(s string) funcsChain {
    f := func() error {
        return fmt.Errorf("don't speek golish")
    }

    return funcsChain{append(chain.funcs, f)}
}

func (chain funcsChain) Execute() (i int, err error) {
    for i, f := range chain.funcs {
        if err := f(); err != nil {
            return i, err
        }
    }

    return -1, nil
}

func main() {
    i, err := Chain().
        Say("Hello, playground").
        TryToSay("go cannot into chains").
        Execute()

    fmt.Printf("i: %d, err: %s", i, err)
}
Varien answered 10/8, 2018 at 6:54 Comment(1)
Worth trying, although adds a lot of boilerplate code.Cuckoo
S
0

You don't actually need channels and/or contexts to get something like this to work. I think this implementation meets all your requirements but needless to say, this leaves a sour taste. Go is not a functional language and it's best not to treat it as such.

package main

import (
    "errors"
    "fmt"
    "strconv"
)

type Res[T any] struct {
    Val  T
    Halt bool
    Err  error
}

// executes arguments until a halting signal is detected
func (r *Res[T]) Chain(args ...func() *Res[T]) *Res[T] {
    temp := r
    for _, f := range args {
        if temp = f(); temp.Halt {
            break
        }
    }

    return temp
}

// example function, converts any type -> string -> int -> string
func (r *Res[T]) funA() *Res[string] {
    s := fmt.Sprint(r.Val)
    i, err := strconv.Atoi(s)
    if err != nil {
        r.Err = fmt.Errorf("wrapping error: %w", err)
    }
    fmt.Println("the function down the pipe is forced to work with Res[string]")

    return &Res[string]{Val: strconv.Itoa(i), Err: r.Err}
}

func (r *Res[T]) funB() *Res[T] {
    prev := errors.Unwrap(r.Err)
    fmt.Printf("unwrapped error: %v\n", prev)

    // signal a halt if something is wrong
    if prev != nil {
        r.Halt = true
    }
    return r
}

func (r *Res[T]) funC() *Res[T] {
    fmt.Println("this one never gets executed...")
    return r
}

func (r *Res[T]) funD() *Res[T] {
    fmt.Println("...but this one does")
    return r
}

func funE() *Res[string] {
    fmt.Println("Chain can even take non-methods, but beware of nil returns")
    return nil
}

func main() {
    r := Res[string]{}
    r.Chain(r.funA, r.funB, r.funC).funD().Chain(funE).funC() // ... and so on
}
Stewardess answered 13/8, 2022 at 20:4 Comment(0)
J
-1

How about this approach: Create a struct that delegates Chain and error, and return it instead of two values. e.g.:

package main

import "fmt"

type Chain struct {
}

type ChainAndError struct {
    *Chain
    error
}

func (v *Chain)funA() ChainAndError {
    fmt.Println("A")
    return ChainAndError{v, nil}
}

func (v *Chain)funB() ChainAndError {
    fmt.Println("B")
    return ChainAndError{v, nil}
}

func (v *Chain)funC() ChainAndError {
    fmt.Println("C")
    return ChainAndError{v, nil}
}

func main() {
    fmt.Println("Hello, playground")
    c := Chain{}
    result := c.funA().funB().funC() // line 24
    fmt.Println(result.error)
}
Jemina answered 4/12, 2014 at 16:18 Comment(2)
An error here won't stop the chain from executing, per the original question. Even worse, the only error here that counts is that of funC(); the errors returned by funA() and funB() are ignored.Lesleylesli
How about piling up the errors in a slice for investigation-purpose, and in anyway, regard the result as erroneous as the slice has 1 or more elements?Goebbels

© 2022 - 2024 — McMap. All rights reserved.