Golang Go语言中如何设计一个优雅的链式操作?

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

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 ,希望能得到大家的一些中肯建议,感谢!


Golang Go语言中如何设计一个优雅的链式操作?

更多关于Golang Go语言中如何设计一个优雅的链式操作?的实战教程也可以访问 https://www.itying.com/category-94-b0.html

19 回复

你最后加一个
go<br>money.new(123.12, "USD").Add(456).Mul(123).Done().toStr()<br>
有问题直接在 Done 那里 panic

更多关于Golang Go语言中如何设计一个优雅的链式操作?的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


if _, err := money.new(123.12, “USD”).Add(456).Mul(123).toStr(); err != nil {
// 处理错误
}
每个方法返回结构体指针和 error 就行了啊。

首先,Go 其实是希望每个错误都能处理。不过如果非要这么做,也不是不行,但是有点丑

go<br>import "fmt"<br><br>type Money struct {<br> Amount int64<br> Currency string<br> LastError error<br>}<br><br>func (m Money)Add(val Money) Money {<br> if m.LastError != nil || val.LastError != nil {<br> return m<br> }<br> if m.Currency != val.Currency {<br> m.LastError = fmt.Errorf("currency mismatch")<br> return m<br> }<br> return Money{Amount: m.Amount + val.Amount, Currency: m.Currency}<br>}<br><br>func main() {<br> a := Money{Amount: 100, Currency: "USD"}<br> b := Money{Amount: 200, Currency: "USD"}<br> c := Money{Amount: 200, Currency: "HKD"}<br><br> fmt.Println(a.Add(b))<br> fmt.Println(a.Add(c))<br>}<br>

val, err := money.new(123.12, “USD”).Add(456).Mul(123).toStr()

每个方法都进行单独的 error 捕捉,如果发生错误了存一个 lastError ,后续的方法判断之前是否有 lastError ,有就不做任何计算返回,最后的 toStr 就判断有没有 lastError ,有的话返回 nil, lastError ,否则返回 result, nil

推荐在最后一个函数返回错误, 如果中间状态时发现出错,则暂存错误.

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


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

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

两个字:指针

go 1.20 加上了 errors.Join() 函数了 https://pkg.go.dev/errors#Join ,直接最后对错误做判断处理。1.19 之前可以使用 multierr.Combine(),import “go.uber.org/multierr

推荐将前面加减乘除的链式操作设计为只是 build 表达式,最后的 toStr 才是真正执行计算.

可以参考 ent https://entgo.io/zh/docs/crud 的实现

惊呆了,这种语言上的设计,和 python 的缩进语法有的一拼,估计作者自己都吐了😂

你可以这样写

<br>package main<br><br>import "log"<br><br>type OP string<br><br>const (<br> Add OP = "add"<br> Mul OP = "mul"<br>)<br><br>type step struct {<br> op OP<br> value int64<br>}<br><br>func NewMoney(amount int64) *Money {<br> return &amp;Money{<br> amount: amount,<br> }<br>}<br><br>type Money struct {<br> amount int64<br> steps []step<br>}<br><br>func (r *Money) Add(v int64) *Money {<br> r.steps = append(r.steps, step{<br> op: Add,<br> value: v,<br> })<br> return r<br>}<br><br>func (r *Money) Mul(v int64) *Money {<br> r.steps = append(r.steps, step{<br> op: Mul,<br> value: v,<br> })<br> return r<br>}<br><br>func (r *Money) Run() (int64, error) {<br> for _, step := range r.steps {<br> switch step.op {<br> case Add:<br> r.amount += step.value<br> case Mul:<br> r.amount -= step.value<br> }<br> }<br> return r.amount, nil<br>}<br><br>func main() {<br> val, err := NewMoney(123).Add(456).Mul(123).Run()<br> if err != nil {<br> log.Fatal(err)<br> }<br><br> log.Println(val)<br>}<br><br>

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

官方 blog 有篇文章讲过这种设计模式: https://go.dev/blog/errors-are-values

不需要每个方法都执行,前面的可以只构建参数,只在某几个最后调用 exec 的方法里处理 类似 9 楼的方案

可以参考下 gorm 的链式实现方式

链式不直观清晰,不容易理解,链式本身就很丑。

总结:OP 什么破品味!少制造这种辣鸡封装!

orm 和 httputil 中链式还是挺方便的

链式调用实在是丑得一逼!实现链式调用非常简单,每个方法都返回 this 就行。

那啥比较好看

在Go语言中设计优雅的链式操作,主要依赖于方法接收者的灵活运用,特别是值接收者和指针接收者的选择,以及返回值的处理。以下是一些关键点:

  1. 使用指针接收者: 链式操作通常涉及多个方法的连续调用,且每个方法都可能修改接收者的状态。因此,使用指针接收者可以避免每次方法调用时都复制整个结构体,从而提高效率并允许状态共享。

  2. 返回接收者本身: 每个方法应该返回接收者本身(通常是*SelfType),以便可以进行后续的方法调用。这是实现链式操作的关键。

  3. 提供有意义的默认值: 为了使链式操作更加灵活,可以为方法参数提供有意义的默认值,从而允许调用者只设置必要的参数。

  4. 错误处理: 链式操作中的错误处理是一个挑战。可以考虑使用error类型的返回值,并在链的末尾统一检查错误,或者使用panic/recover机制来处理不可恢复的错误(但需谨慎使用)。

  5. 文档和示例: 提供清晰的文档和示例代码,以帮助其他开发者理解和使用链式操作。

示例代码:

type Builder struct {
    // 结构体字段
}

func (b *Builder) Method1(param1 string) *Builder {
    // 处理逻辑
    return b
}

func (b *Builder) Method2(param2 int) *Builder {
    // 处理逻辑
    return b
}

// 使用链式操作
builder := &Builder{}
result := builder.Method1("value1").Method2(123)

这样的设计可以使代码更加简洁和易读。

回到顶部