[转]在Go中实现自己的future语句
(原文:https://levelup.gitconnected.com/build-your-own-future-in-go-f66c568e9a7a)Go 编程语言的主要特点之一是它的同名go
语句。不过,在我看来,该go
声明也是其主要缺点之一。这不仅仅是我这么认为(https://vorpus.org/blog/notes-on-structured-concurrency-or-go-statement-considered-harmful/)。
与表达式不同,语句不产生任何结果。在 Go 中,启动一个新的 goroutine 非常容易。但是你如何得到它的结果呢?你怎么知道它是否可能出错?你如何等待它完成?如果不再需要它的结果,如何取消它?
熟悉 Go 的人会说:嗯,显然,使用通道。但是 Go 中的 channel 仍然是一个低级结构。首先,要拥有一个可以产生结果或错误的 goroutine,而且它也是可取消的,你需要其中的三个。您可能认为Context
这有助于满足第三个要求。但Context
还是暴露渠道:Done()
只是<-chan struct{}
这有什么问题?您拥有的渠道越多——问题也就越多。通道可能会死锁。渠道可能会恐慌。甚至在您开始编写业务逻辑之前,您就已经有所有需要处理的低级问题。
而且只写一次是不够的。你将不得不一遍又一遍地重复它。因为很可能,两个不同的 goroutine 会返回两种不同类型的结果。这意味着,两种不同的渠道类型。这意味着,如果没有泛型,您要么多次复制代码,要么求助于使用interface{}
和运行时强制转换,这完全打破了类型安全的想法。您必须在两个糟糕的解决方案之间做出选择,因为这go
是一个陈述,而不是一个表达式。
好吧,在 Go 语言将泛型作为实验性功能引入之前,情况确实如此。我已经简要地写过一次关于它们的文章(https://medium.com/swlh/experimenting-with-generics-in-go-39ffa155d6a1),这一次,我想展示泛型如何帮助我们解决 Go 设计的最大缺陷之一。
我们将实现一个延迟值设计模式,它在不同的语言和框架中被称为 Future、Promise 和一堆其他名称。我将它称为 Future,就像 Java 一样。
延迟值要么是急切的,要么是懒惰的。这意味着它们要么在创建后立即开始执行,要么仅在某些事情触发它们时开始执行。由于go
语句本质上是急切的,我更喜欢急切的执行。
我们的 Future 会有以下方法:
Get()
阻塞当前 goroutine 直到获得 Future 的结果Cancel()
停止执行我们的未来
在 Go 术语中,它将是一个具有两种方法的接口:
type Future[type T] interface {
Get() Result[T]
Cancel()
}
请注意,我将使用方括号来表示泛型类型。它们没有记录在提案中,但Go2Playground支持它们,这是我从这篇文章中了解到的。我发现这种类似 Scala 的语法比圆括号更容易混淆。
Result
是另一个接口,它包装Success
了 typeS
或 a Failure
:
type Result[type S] interface {
Success() S
Failure() error
}
为了支持 Result,我们需要一个结构来保存它的数据:
type result[type S] struct {
success S
failure error
}
查看结构,实现两个 Result 接口方法应该是微不足道的:
func (this *result(S)) Success() S {
return this.success
}
func (this *result(S)) Failure() error {
return this.failure
}
我们可以简单地将结构本身公开,并避免使用接口,从而节省几行代码,但接口提供了更清晰的 API。
将来,打印结果的内容也会很方便,因此我们将为此实现 Stringer 接口:
func (this *result(S)) String() string {
if this.failure != nil {
return fmt.Sprintf("%v", this.failure)
} else {
return fmt.Sprintf("%v", this.success)
}
}
到目前为止,它应该非常简单。现在让我们讨论我们的结构支持Future
将需要哪些数据。
有这个结构:
type future[type T] struct {
...
}
关于 的状态,我们需要了解什么Future
?
首先,我们想把结果保存在某个地方:
type future[type T] struct {
result *result[T]
...
}
了解 Future 是否已经完成也很有用:
type future[type T] struct {
...
completed bool
...
}
如果它还没有完成,我们需要一种方法来等待它。Go 中的一种常见方法是使用通道:
type future[type T] struct {
...
wait chan bool
...
}
我们的最后一个要求是能够取消Future
. 为此,我们将使用Context
, 返回一个我们需要调用以取消它的函数:
type future[type T] struct {
...
cancel func()
}
但参考Context
自身也很有用:
type future[type T] struct {
...
ctx context.Context
cancel func()
}
就是这样,这就是我们Future
现在需要的所有数据。
type future[type T] struct {
result *result[T]
complete bool
wait chan bool
ctx context.Context
cancel func()
}
现在让我们实现这两种Future
方法。
由于我们正在使用Context
,取消我们Future
变得微不足道:
func (this *future[T]) Cancel() {
this.cancel()
}
现在让我们讨论我们Get()
应该处理哪些情况。
- 在
Future
已经完成的工作。然后我们应该简单地返回结果,无论是成功还是失败 - 还
Future
没有完成它的工作。然后我们应该等待,阻塞调用 goroutine,当结果准备好时,我们应该返回它 - 将
Future
在此期间取消。我们应该返回一个错误,表明
映射了这三种情况后,我们得出了以下方法:
func (this *future[T]) Get() Result[T] {
if this.completed {
return this.result
} else {
fmt.Println("Need to wait...")
select {
case <-this.wait:
return this.result
case <-this.ctx.Done():
return &result(T){failure: this.ctx.Err()}
}
}
}
已经完成的 Future 的案例非常简单。我们只返回缓存的结果。
如果它还没有完成,我们使用wait
通道等待它。
也可能存在我们的 Future 通过取消上下文而被取消的情况。我们会通过检查ctx.Done()
频道知道这一点。
这就是实现处理结果的不同用例。
接下来,让我们看看我们如何构建我们的 Future。
我们的 Future 需要执行任意一段代码。代码本身可能返回泛型类型的结果或错误。我们的构造函数将简单地返回相同泛型类型的 Future。
func NewFuture[type T](f func() (T, error)) Future[T] {
...
}
请注意泛型如何让我们现在定义输入和输出类型之间的强大关系。我们Future
保证返回与我们提供的构造函数的任意函数相同的类型。不再需要interface{}
不安全地使用和投射。
接下来,我们要初始化我们的Future
:
fut := &future[T]{
wait: make(chan bool),
}
fut.ctx, fut.cancel = context.WithCancel(context.Background())
...
return fut
我们创建了一个Context
,以便我们Future
可以取消,以及一个通道,以便我们可以等待它以并发方式完成。
您可能需要考虑传递Context
给 的构造函数Future
,而不是自己创建它。为了示例的简洁,我省略了这一点。
最后,我们需要对我们推迟的任意一段代码做一些事情:
go func() {
success, failure := f()
fut.result = &result[T]{success, failure}
fut.completed = true
fut.wait <- true
close(fut.wait)
}()
在这里,我们在一个新的 goroutine 中执行该函数,获取其结果,并将我们的标记Future
为已完成。
Channel 应该只使用一次,所以最好关闭它。
根据您的用例,您可能需要考虑使用工作池而不是为每个未来生成 goroutine。
现在让我们看看它是如何工作的。
首先,我们希望看到我们的 Future 能够返回一个结果:
f1 := NewFuture(func() (string, error) {
time.Sleep(1000)
return "F1", nil
})
fmt.Printf("ready with %v \n", f1.Get())
// Need to wait...
// ready with F1
到目前为止,看起来不错。
但是,如果我们再次尝试获得结果怎么办?
fmt.Printf("trying again with %v \n", f1.Get())
// trying again with F1
请注意,它现在不会打印“需要等待”,因为结果已经被记住了。
如果函数返回错误,我们的 Future 会如何表现?
f2 := NewFuture(func() (string, error) {
time.Sleep(1000)
return "F2", fmt.Errorf("something went wrong")
})
fmt.Printf("ready with %v \n", f2.Get())
// Need to wait...
// ready with something went wrong
不错,似乎错误也得到了正确处理。
最后,取消呢?
f3 := NewFuture(func() (string, error) {
time.Sleep(100)
fmt.Println("I'm done!")
return "F3", nil
})
f3.Cancel()
fmt.Printf("ready with %v \n", f3.Get())
// Need to wait...
// ready with context canceled
请注意“我完成了!” 永远不会打印,因为我们丢弃了这个 Future 的结果。
结论
Go 中的泛型可能有助于解决许多由go
语句而不是表达式引起的问题。
多亏了它们,我们可以使用延迟值作为我们的并发原语,就像许多其他语言一样。这意味着我们现在可以:
- 轻松访问 goroutine 结果和错误
- 编写类型安全的代码,这仍然是可重用和通用的
- 停止搞乱低级并发原语,例如通道
- 可以
go
完全停止使用语句
脚注
完整的代码示例可以在这里找到:https://github.com/AlexeySoshin/go2future
/*******************************/
为什么golang下划线结构字段存在(https://paulyeo21.medium.com/golang-underscore-struct-field-f0aecabc72ae)
如果您编写了一些 go 或深入研究了一个维护良好的 go 项目,您可能会看到带有下划线字段的结构:
type ListTablesInput struct {
_ struct{} `type:"structure"`
ExclusiveStartTableName *string `min:"3" type:"string"
Limit *int64 `min:"1" type:"integer"`
}
在结构上使用 _ 字段的目的是在初始化结构时强制使用键控字段。
input := ListTablesInput{
ExclusiveStartTableName: "table-name",
Limit: 100,
}
// instead of
input := ListTablesInput{"table-name", 100}
如果您尝试在没有字段名称的情况下初始化结构体,您将收到类似“不能在字段值中用作类型结构{}”这样的错误信息。
的enforcin的一大优势摹初始化结构时,这个规则是添加到任何结构新的领域将不引入重大更改。例如,添加了一个新字段,例如:
type ListTablesInput struct {
_ struct{} `type:"structure"`
ExclusiveStartTableName *string `min:"3" type:"string"`
Limit *int64 `min:"1" type:"integer"`
// New field
Option string
}
那么从我们前面的例子来看,没有关键字段的结构初始化将无法编译,因为它现在需要添加新字段。
// valid still
ListTablesInput{
ExclusiveStartTableName: "table-name",
Limit: 100,
}
// fails to compile
ListTablesInput{"table-name", 100}
- 点赞
- 收藏
- 关注作者
评论(0)