Go方法链和错误处理

25

我希望在Go中创建一个方法链接API。 在我找到的所有示例中,链接操作似乎总是成功的,但我无法保证。 因此,我尝试扩展这些以添加错误返回值。

如果我这样做

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
}

编译器告诉我chain-err-test.go:24: multiple-value c.funA() in single-value context,并且无法编译。有没有一种好的方法,使得funcA、funcB和funcC可以报告错误并停止该链?


你可以使用 panic,但这意味着你必须在每个方法或其根部进行恢复。你也可以将 Chain 对象设为有状态的,并带有一个错误来检查每个方法。 - Not_a_Golfer
@Not_a_Golfer 是的,但我想知道是否有一种好的惯用方法来做到这一点。在我的世界里,错误条件无处不在,在某些地方(即运算符,如http://www.golangpatterns.info/object-oriented/operators),这种链接应该提供一个很好的API。(该页面上的第一个示例即不是一个好的API - 要么你有大量的函数适用于所有类型,要么你有一个开关,其中编译器无法检测到错误类型) - johannes
1
我记得一个更好的API涉及返回函数的函数,就像http://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis(在http://commandcenter.blogspot.com.au/2014/01/self-referential-functions-and-design.html之后完成)。但它并不完全是“链接”。 - VonC
1
我知道这不是你想要的答案,但惯用的做法是避免链式调用。链式调用并没有错,但它是使用错误值而不是异常的习惯用法的结果。 - weberc2
@weberc2 如果没有更好的方法,那么你下面的回答可能是一个不错的选择,我希望自己没有因为对Go语言缺乏经验而忽略了某些东西。 - johannes
显示剩余2条评论
6个回答

43
有没有一种好方法让funcA、funcB和funcC报告一个错误并停止这个链条呢?很遗憾,没有好的解决方案。解决方法过于复杂(添加错误通道等),成本超出了收益。在Go中,方法链不是一个习惯用语(至少对于可能出错的方法来说不是)。这不是因为方法链有什么特别的问题,而是返回错误而不是引发panic的习惯的结果。其他答案都是解决方法,但没有一种是习惯用语。

我可以问一下吗,不在Go中使用方法链是否因为我们在Go中返回错误的结果,还是通常由于具有多个方法返回的后果呢? 好问题,但这不是因为Go支持多返回值。Python支持多返回值,Java也可以通过Tuple<T1, T2>类进行多返回值;这两种语言都普遍使用方法链。这些语言之所以能够做到这一点,是因为它们以习惯用语的方式通过异常来传递错误。异常会立即停止方法链并跳转到相关的异常处理程序。这是Go开发人员明确选择返回错误而不是采用异常的行为。

很好的回答(+1)。我可以问一下,在Go语言中链式调用方法不是惯用方式,是因为返回错误会产生后果,还是由于有多个方法返回值的普遍结果呢? - user1503949
2
谢谢你的解释,真是明了。如果我能点赞两次就好了 ;) - user1503949
为了补充你对@user1503949问题的回答——如果在Go中一个方法可以有多个接收器,那会怎样呢?你可以在方法链中包含错误,对吧?如果是这样的话,我认为正是缺少这个特性导致链式调用不符合惯例。在假设的多接收器Go中包含错误将要求后续方法处理前面方法的错误,而在我的看法中,对于Go中的链式调用来说,这是很有意义的。(编辑:话虽如此,我相信这样做会以其他方式破坏语言) - Eric Dubé
@EricDubé 我认为多接收者没有意义;否则你将会像这样调用函数:(foo, nil).Bar(),只是为了实现方法链。这个问题的真正解决方案(如果它是一个问题的话)是单子。定义一个短路错误单子并像这样链接:foo >>= bar >>= baz(其中bar接受FooResult,baz接受BarResult),但这需要更复杂的类型系统,其中有优点和缺点(特别是学习曲线更陡峭,程序之间的一致性较差,更好的优化编译器来维护性能)。 - weberc2
(foo, nil) 有什么问题吗?或者说,(foo, error(nil))?你只需要在链中的第一个方法之前这样做,我认为这是一件好事,因为它清楚地表明了链式调用正在发生。 - Eric Dubé

12
你可以尝试这样做: 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)
}

3
如果您控制代码并且函数签名相同,您可以编写类似以下内容的代码:
func ChainCall(fns ...func() (*Chain, error)) (err error) {
    for _, fn := range fns {
        if _, err = fn(); err != nil {
            break
        }
    }
    return
}

playground


为什么要返回原始的 *Chain 对象呢?你并没有对它进行任何操作。即便如此,这是一个聪明的解决方案,但它比写出 X={A, B, C} 分别执行 if err := funX(); err != nil { /* do something */ } 更难以阅读。 - weberc2
主要是因为我太懒了,不想重写代码,而且想展示一个可以与他当前的代码一起使用的示例 ;) - OneOfOne
1
其实这个想法很棒!"Chain"可能是指正在操作的某个对象的引用,强制所有函数始终在同一个对象上运行。不足之处是难以给每个函数传递不同的参数。传递调用真实函数的一堆匿名函数可能会起作用,但我认为这样做会非常冗长。 - Eric Dubé

1
你可以通过收集一组函数来使你的链式调用变得更加灵活。
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)
}

值得一试,尽管会增加很多样板代码。 - metalim

0

实际上,您不需要通道和/或上下文来使类似的东西工作。我认为这个实现满足了您所有的要求,但不用说,这让人感到不舒服。Go不是一种函数式语言,最好不要将其视为这样。

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
}

-1
这种方法怎么样:创建一个结构体,委托Chainerror,并返回它而不是两个值。例如:
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)
}

2
根据原始问题,这里的错误不会停止链式执行。更糟糕的是,这里唯一重要的错误是funC()的错误;funA()funB()返回的错误被忽略了。 - weberc2
1
把错误堆积在一个切片中以便进行调查,无论如何,只要切片有1个或更多元素,就将结果视为错误的。 - Bert Verhees

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接