【问题标题】:Go method chaining and error handlingGo 方法链和错误处理
【发布时间】:2014-12-04 15:26:01
【问题描述】:

我想在 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 是的,但我想知道是否有一种很好的惯用方式来做到这一点。在我的世界中,错误条件无处不在,并且在某些地方(即golangpatterns.info/object-oriented/operators 中的运算符)这样的链接应该提供一个很好的API。 (该页面上的第一个示例,即不是一个很好的 API - 要么你有大量适用于所有类型的函数,要么编译器没有检测到错误类型的开关)
  • 我记得有一个更好的 API 涉及返回函数的函数,如 dave.cheney.net/2014/10/17/functional-options-for-friendly-apis(在 commandcenter.blogspot.com.au/2014/01/… 之后完成)。但它并不完全是“链接”。
  • @johannes 我做了类似的事情,但使用通道链接输出流,并且错误只是关闭了通道,结束了整个链。不过,这可能不是您所需要的。
  • 我知道这不是您要寻找的答案,但惯用的做法是避免链接。链接没有错,但这是使用错误值而不是异常的习惯用法的结果。

标签: error-handling go chaining


【解决方案1】:

有没有什么好方法可以让funcA、funcB和funcC报错并停止该链?

很遗憾,不,您的问题没有好的解决方案。变通方法非常复杂(添加错误通道等),成本超过收益。

方法链接在 Go 中不是一个习惯用法(至少对于可能出错的方法来说不是)。这并不是因为方法链有什么特别的错误,而是返回错误而不是恐慌的习惯用法的结果。其他答案是解决方法,但没有一个是惯用的。

我能问一下,在 Go 中链接方法不是惯用的,因为我们在 Go 中返回错误的后果,还是更普遍地是多个方法返回的后果?

问得好,但这并不是因为 Go 支持多重返回。 Python 支持多个返回,Java 也可以通过 Tuple<T1, T2> 类;方法链在两种语言中都很常见。这些语言可以侥幸逃脱的原因是它们习惯性地通过异常来传达错误。异常会立即停止方法链并跳转到相关的异常处理程序。这是 Go 开发人员特别试图通过选择返回错误来避免的行为。

【讨论】:

  • 不错的答案 (+1)。我可以问一下,在 Go 中链接方法不是惯用的,因为我们在 Go 中返回错误的结果,还是更普遍地是多个方法返回的结果?
  • 谢谢你;这是一个很好的澄清。如果我能投两次票就好了;)
  • 补充您对@user1503949 问题的回答- 如果您可以在Go 中为一个方法设置多个接收器怎么办?然后你可以在方法链中包含错误,对吧?如果是这样的话,我认为缺少此功能是链接不习惯的原因。在假设的多接收器 Go 中包含错误将需要一个后续方法来处理前面方法的错误,在我看来,这对于 Go 中的链来说是有意义的。 (编辑:也就是说我确信这会以其他方式破坏语言)
  • @EricDubé 我认为多接收器没有意义;否则你会调用这样的函数:(foo, nil).Bar() 只是为了获得方法链。这个问题的真正解决方案(就它是一个问题而言)是单子。定义一个短路错误单子和链,如下所示:foo >>= bar >>= baz(其中 bar 采用 FooResult,而 baz 采用 BarResult),但这需要更复杂的类型系统,其优点和缺点(特别是更陡峭的学习曲线和更少的程序一致性,更不用说更好地优化编译器来保持性能)。
  • (foo, nil) 有什么问题?或者,更确切地说,(foo, error(nil))?您只需在链中的第一个方法之前执行此操作,IMO 将是一件好事,因为它清楚地表明正在发生链接。
【解决方案2】:

你可以这样尝试: 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 对象?你没有用它做任何事情。尽管如此,这仍然是一个聪明的解决方案,但这种聪明的方法比写if err := funX(); err != nil { /* do something */ } for X={A, B, C} 的可读性差。
    • @weberc2 主要是因为我懒得重写代码,想展示一个适用于他当前代码的示例;)
    • 其实这太棒了! *Chain 可能是对某个正在被操作的对象的引用,从而强制所有函数始终对同一事物进行操作。不足之处在于给每个函数提供不同的参数更加困难。传递一堆调用真实函数的匿名函数是可行的,但我认为它会非常冗长。
    【解决方案4】:

    你可以通过收集一些函数来让你的链变得懒惰

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

    【讨论】:

    • 值得一试,虽然增加了很多样板代码。
    【解决方案5】:

    这种方法怎么样:创建一个代表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)
    }
    

    【讨论】:

    • 根据原始问题,此处的错误不会阻止链执行。更糟糕的是,这里唯一重要的错误是funC()funA()funB() 返回的错误将被忽略。
    • 如何将错误堆积在一个切片中以进行调查,并且无论如何,由于切片具有1个或多个元素,因此将结果视为错误?
    最近更新 更多