V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
The Go Programming Language
http://golang.org/
Go Playground
Go Projects
Revel Web Framework
dyingfire
V2EX  ›  Go 编程语言

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

  •  
  •   dyingfire · 292 天前 · 2294 次点击
    这是一个创建于 292 天前的主题,其中的信息可能已经有所发展或是发生改变。

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

    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 条回复    2023-07-11 10:25:15 +08:00
    phpfpm
        1
    phpfpm  
       292 天前
    你最后加一个
    ```go
    money.new(123.12, "USD").Add(456).Mul(123).Done().toStr()
    ```
    有问题直接在 Done 那里 panic
    pota
        2
    pota  
       292 天前
    if _, err := money.new(123.12, "USD").Add(456).Mul(123).toStr(); err != nil {
    // 处理错误
    }
    每个方法返回结构体指针和 error 就行了啊。
    picone
        3
    picone  
       292 天前
    首先,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
        4
    lisxour  
       292 天前
    val, err := money.new(123.12, "USD").Add(456).Mul(123).toStr()

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

    ```
    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
        6
    realpg  
       292 天前
    如果是 java 来实现,可以很好的 try catch 包起来,但是放到 go 里面,因为只能通过 return value 来判断,感觉很难设计出都能兼容上面方式的代码。

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

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

    ```
    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
        12
    Kumo31  
       292 天前
    我会使用第一种,「错误是值」,在连续操作中错误不应该打断控制流,而是记录错误,最后再返回。典型例子是 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
        13
    cnoder  
       292 天前
    不需要每个方法都执行,前面的可以只构建参数,只在某几个最后调用 exec 的方法里处理 类似 9 楼的方案
    lc5900
        14
    lc5900  
       292 天前
    可以参考下 gorm 的链式实现方式
    lesismal
        15
    lesismal  
       292 天前
    链式不直观清晰,不容易理解,链式本身就很丑。

    总结:OP 什么破品味!少制造这种辣鸡封装!
    trzzzz
        16
    trzzzz  
       291 天前
    orm 和 httputil 中链式还是挺方便的
    xuanbg
        17
    xuanbg  
       291 天前
    链式调用实在是丑得一逼!实现链式调用非常简单,每个方法都返回 this 就行。
    proxytoworld
        18
    proxytoworld  
       291 天前
    @xuanbg 那啥比较好看
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   2822 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 43ms · UTC 14:57 · PVG 22:57 · LAX 07:57 · JFK 10:57
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.