The Go Programming Language
› http://golang.org/
› Go Playground
Go Projects
› Revel Web Framework
dyingfire

对于 go 来说,如何设计一个优雅的链式操作?

  •  
  •   dyingfire · Jul 10, 2023 · 3896 views
    This topic created in 1214 days ago, the information mentioned may be changed or developed.

    假设以最简单的加减乘除举例

    type Money struct {
    	amount int64
        currency string
    }
    

    想要实现

    money.new(123.12, "USD").Add(456).Mul(123).toStr()

    这里面,先初始化成 money ,然后做运算,最后 toStr 成科学计数法的格式。当然,每一步都可以拆开,比如先初始化,然后 add ,得到的 money 最后通过参数传给其他方法运行,在另外的方法里面可以继续计算或者格式化输出。

    这里,new 可能报错,比如溢出、格式不正确,add 、mul 都有可能溢出。

    如果是 java 来实现,可以很好的 try catch 包起来,但是放到 go 里面,因为只能通过 return value 来判断,感觉很难设计出都能兼容上面方式的代码。

    要么就是在 tostr 最后计算的时候判断前面所有报错

    if m, err := money.new(123.12, "USD"); err != nil {
    	m := m.Add(456).Mul(123)
        if val, err := m.toStr(); err != nil {
        	...
        }
    }
    

    要么就是每一步都判断,错误存入 money 对象中,需要的时候判断

    m := money.new(123.12, "USD")
    if Errors.isError(m) {
    	... 
    }
    
    m := Add(456).Mul(123);
    if Errors.isError(val) {
    	... 
    }
    
    val := m.toStr()
    if Errors.isError(val) {
    	... 
    }
    
    or 
    
    val := money.new(123.12, "USD").Add(456).Mul(123).toStr()
    if Errors.isError(val) {
    	... 
    }
    

    刚学习 go ,希望能得到大家的一些中肯建议,感谢!

    18 replies  •  2023-07-11 10:25:15 +08:00
    phpfpm
    phpfpm
    Jul 10, 2023
    1
    你最后加一个
    ```go
    money.new(123.12, "USD").Add(456).Mul(123).Done().toStr()
    ```
    有问题直接在 Done 那里 panic
    pota
    pota
    Jul 10, 2023
    2
    if _, err := money.new(123.12, "USD").Add(456).Mul(123).toStr(); err != nil {
    // 处理错误
    }
    每个方法返回结构体指针和 error 就行了啊。
    picone
    picone
    Jul 10, 2023
    3
    首先,Go 其实是希望每个错误都能处理。不过如果非要这么做,也不是不行,但是有点丑

    ```go
    import "fmt"

    type Money struct {
    Amount int64
    Currency string
    LastError error
    }

    func (m Money)Add(val Money) Money {
    if m.LastError != nil || val.LastError != nil {
    return m
    }
    if m.Currency != val.Currency {
    m.LastError = fmt.Errorf("currency mismatch")
    return m
    }
    return Money{Amount: m.Amount + val.Amount, Currency: m.Currency}
    }

    func main() {
    a := Money{Amount: 100, Currency: "USD"}
    b := Money{Amount: 200, Currency: "USD"}
    c := Money{Amount: 200, Currency: "HKD"}

    fmt.Println(a.Add(b))
    fmt.Println(a.Add(c))
    }
    ```
    lisxour
    lisxour
    Jul 10, 2023
    4
    val, err := money.new(123.12, "USD").Add(456).Mul(123).toStr()

    每个方法都进行单独的 error 捕捉,如果发生错误了存一个 lastError ,后续的方法判断之前是否有 lastError ,有就不做任何计算返回,最后的 toStr 就判断有没有 lastError ,有的话返回 nil, lastError ,否则返回 result, nil
    aapeli
    aapeli
    Jul 10, 2023
    5
    推荐在最后一个函数返回错误, 如果中间状态时发现出错,则暂存错误.

    ```
    val,err := money.new(123.12, "USD").Add(456).Mul(123).Done()
    if err != nil {
    }
    ```


    参考 gorm 的错误处理 https://gorm.io/zh_CN/docs/error_handling.html

    @dyingfire
    realpg
    realpg
    Jul 10, 2023
    6
    如果是 java 来实现,可以很好的 try catch 包起来,但是放到 go 里面,因为只能通过 return value 来判断,感觉很难设计出都能兼容上面方式的代码。

    两个字:指针
    yazinnnn
    yazinnnn
    Jul 10, 2023
    7
    ryan961
    ryan961
    Jul 10, 2023❤️1
    8
    go 1.20 加上了 errors.Join() 函数了 https://pkg.go.dev/errors#Join ,直接最后对错误做判断处理。1.19 之前可以使用 multierr.Combine(),import "go.uber.org/multierr"
    SimbaPeng
    SimbaPeng
    Jul 10, 2023
    9
    推荐将前面加减乘除的链式操作设计为只是 build 表达式,最后的 toStr 才是真正执行计算.

    可以参考 ent https://entgo.io/zh/docs/crud 的实现
    xiangyuecn
    xiangyuecn
    Jul 10, 2023
    10
    惊呆了,这种语言上的设计,和 python 的缩进语法有的一拼,估计作者自己都吐了😂
    dushixiang
    dushixiang
    Jul 10, 2023
    11
    你可以这样写

    ```
    package main

    import "log"

    type OP string

    const (
    Add OP = "add"
    Mul OP = "mul"
    )

    type step struct {
    op OP
    value int64
    }

    func NewMoney(amount int64) *Money {
    return &Money{
    amount: amount,
    }
    }

    type Money struct {
    amount int64
    steps []step
    }

    func (r *Money) Add(v int64) *Money {
    r.steps = append(r.steps, step{
    op: Add,
    value: v,
    })
    return r
    }

    func (r *Money) Mul(v int64) *Money {
    r.steps = append(r.steps, step{
    op: Mul,
    value: v,
    })
    return r
    }

    func (r *Money) Run() (int64, error) {
    for _, step := range r.steps {
    switch step.op {
    case Add:
    r.amount += step.value
    case Mul:
    r.amount -= step.value
    }
    }
    return r.amount, nil
    }

    func main() {
    val, err := NewMoney(123).Add(456).Mul(123).Run()
    if err != nil {
    log.Fatal(err)
    }

    log.Println(val)
    }

    ```
    Kumo31
    Kumo31
    Jul 10, 2023
    12
    我会使用第一种,「错误是值」,在连续操作中错误不应该打断控制流,而是记录错误,最后再返回。典型例子是 Scanner 的设计:
    ```go
    scanner := bufio.NewScanner(input)
    for scanner.Scan() {
    token := scanner.Text()
    // process token
    }
    if err := scanner.Err(); err != nil {
    // process the error
    }
    ```

    官方 blog 有篇文章讲过这种设计模式: https://go.dev/blog/errors-are-values
    cnoder
    cnoder
    Jul 10, 2023
    13
    不需要每个方法都执行,前面的可以只构建参数,只在某几个最后调用 exec 的方法里处理 类似 9 楼的方案
    lc5900
    lc5900
    Jul 10, 2023
    14
    可以参考下 gorm 的链式实现方式
    lesismal
    lesismal
    Jul 10, 2023
    15
    链式不直观清晰,不容易理解,链式本身就很丑。

    总结:OP 什么破品味!少制造这种辣鸡封装!
    trzzzz
    trzzzz
    Jul 10, 2023
    16
    orm 和 httputil 中链式还是挺方便的
    xuanbg
    xuanbg
    Jul 11, 2023
    17
    链式调用实在是丑得一逼!实现链式调用非常简单,每个方法都返回 this 就行。
    proxytoworld
    proxytoworld
    Jul 11, 2023
    18
    @xuanbg 那啥比较好看
    About   ·   Help   ·   Advertise   ·   Blog   ·   API   ·   FAQ   ·   Privacy   ·   Solana   ·   4746 Online   Highest 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 64ms · UTC 07:58 · PVG 15:58 · LAX 23:58 · JFK 02:58
    ♥ Do have faith in what you're doing.