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

在学习 Go 的泛型,写了个模拟 async/await 的小工具,写的过程中发现个问题

  •  
  •   rrfeng · 2022-08-30 21:23:40 +08:00 · 2065 次点击
    这是一个创建于 610 天前的主题,其中的信息可能已经有所发展或是发生改变。

    谁能看出来问题在哪里 #狗头

    代码:

    package future
    
    import (
            "context"
    )
    
    type Future[T any] interface {
            Await() (T, error)
            Cancel()
    }
    
    type future[T any] struct {
            ret    chan result[T]
            ctx    context.Context
            cancel context.CancelFunc
    }
    
    type result[T any] struct {
            dat T
            err error
    }
    
    func Async[T any](fn func() (T, error)) Future[T] {
            return AsyncWithContext(context.Background(), fn)
    }
    
    func AsyncWithContext[T any](ctx context.Context, fn func() (T, error)) Future[T] {
            c := make(chan result[T], 1)
            fctx, cancel := context.WithCancel(ctx)
    
            go func() {
                    ret, err := fn()
                    c <- result[T]{dat: ret, err: err}
            }()
            return &future[T]{ret: c, ctx: fctx, cancel: cancel}
    }
    
    func (f *future[T]) Await() (T, error) {
            var result result[T]
    
            select {
            case <-f.ctx.Done():
                    result.err = f.ctx.Err()
            case ret := <-f.ret:
                    result = ret
            }
    
            f.cancel()
            return result.dat, result.err
    }
    
    func (f *future[T]) Cancel() {
            f.cancel()
    }
    

    用法是:

    f := future.Async(myfunc)
    // 去干其他事
    result, err := f.Await()
    
    第 1 条附言  ·  2022-08-31 17:46:52 +08:00
    这里的问题是:
    如果你等待了足够长的时间(大于用户方法的执行时间和 context 的结束时间)后再调用 Await() 的话,得到的结果是随机的。

    因为 select 两个已经就绪的 channel 是随机选择的。
    24 条回复    2022-09-09 16:22:36 +08:00
    buffzty
        1
    buffzty  
       2022-08-31 11:37:16 +08:00
    1. panic 就 gg
    2. 这里没有限制线程数,如果做成 js 那种 应该是起一个 go 协程做事件循环 然后挨个处理. 可以放在 loop 线程里处理 也可以设置在工作线程中处理
    rrfeng
        2
    rrfeng  
    OP
       2022-08-31 12:34:24 +08:00
    @buffzty 不是这个问题。
    lysS
        3
    lysS  
       2022-08-31 15:54:03 +08:00
    你这样的 cancel 是不会其效果的,比如 fn 是 time.Sleep(time.Secont*30), 那么哪个 goroutin 始终会执行 30s
    lysS
        4
    lysS  
       2022-08-31 15:57:10 +08:00
    要想在自己的逻辑里接入 context, 必须要求业务是可拆分的,执行一段后就去检测是否 cacel
    for i := 0; i < 30; i++ {
    time.Sleep(time.Second)
    select {
    case <-ctx.Done():
    return
    }
    }
    rrfeng
        5
    rrfeng  
    OP
       2022-08-31 15:57:57 +08:00
    @lysS cancel 当然有效果,问题不是在这里。
    lysS
        6
    lysS  
       2022-08-31 15:58:52 +08:00
    还要,泛型和 eface 组合毫无意义,泛型和 iface 组合有较大的性能损失 https://www.infoq.cn/article/xprmcl5qbf6yvdroajyn
    rrfeng
        7
    rrfeng  
    OP
       2022-08-31 16:00:01 +08:00
    @lysS 哦你说这个,那是没法取消 fn 的执行,但是 Await() 是会返回的。这个需要调用方传过来的 fn 里自带 context 。
    rrfeng
        8
    rrfeng  
    OP
       2022-08-31 16:00:53 +08:00
    @lysS interface 那个确实没必要。
    lysS
        9
    lysS  
       2022-08-31 16:03:05 +08:00
    @rrfeng #5 cancel 不会有效果,那个协程还在跑着
    lysS
        10
    lysS  
       2022-08-31 16:03:47 +08:00
    @rrfeng #7 fn 无法取消,那么 context 就无意义
    rrfeng
        11
    rrfeng  
    OP
       2022-08-31 17:43:11 +08:00
    @lysS 那你告诉我使用方定义的 fn 你如何取消?
    pastor
        12
    pastor  
       2022-09-01 14:07:21 +08:00
    协程本身就比 async/await 易用、可读性强,OP 搞这种玩意可以加深下自己对协程之类的玩法,但如果真应用到业务里,那就是坑队友了。

    我昨天看了这个帖子标题手滑点进来都没看内容就直接就给关闭了,今天发现竟然有人回复,就又点进来

    本末倒置的玩法,不值得浪费时间,奉劝各位早点散了吧
    rrfeng
        13
    rrfeng  
    OP
       2022-09-01 17:19:11 +08:00
    @pastor
    现在你有个方法里要调用 10 个没有先后顺序的外部接口,每个要花费 1s ,你会怎么写?
    pastor
        14
    pastor  
       2022-09-01 18:26:46 +08:00
    @rrfeng
    第一,如果没有先后顺序,那有序调用也是满足要求的,for 循环挨个调用就可以
    第二,如果有性能要求,同时去请求 10 个才能满足性能,那 wg.Add(10) go func() { defer wg.Done() ... } 也比 async/await 可读性舒服得多,如果这种异步量大这里可以用协程池而不是直接 go

    对于异步理解比较到位的人二院,async/await 并不比 Promise 之类算是改进,相比于 go 可读性就更不直观了
    pastor
        15
    pastor  
       2022-09-01 18:31:17 +08:00
    还有就是,如果你的业务依赖这种同时多个异步的,最麻烦的地方并不是封装这种 async/await 的绕脑的写法,而是实际场景中每个异步请求可能失败后怎么处理。

    这对于不同的业务场景没有固定答案,比如爬虫或者什么,失败了也影响不大;但是对于具有事务性要求的业务,这种同时依赖多个异步远不如串行顺序处理好。对性能有很高要求的八成也应该是依赖自家的基础设施,这种如果还能设计成同时多个异步,那说明你们整体架构已经出问题了、比如微服务拆分得非常不合理,这种要治病得从架构顶层往下梳理而不是脚疼医脚。
    pastor
        16
    pastor  
       2022-09-01 18:35:44 +08:00
    go 的哲学,就是让大家从语法语义中解放出来,这种 async/await 的设计,其实本质上都不算是 lib 封装了,而是更偏于语法语义的语法糖的设计。不管花多少时间玩这种东西,到头来总有一天会想明白,发现竹篮打水。越早回头是岸越划算
    pastor
        17
    pastor  
       2022-09-01 18:36:37 +08:00
    @pastor #14 "二院" -> "而言"
    rrfeng
        18
    rrfeng  
    OP
       2022-09-02 11:20:35 +08:00
    @pastor 要不是楼主是我,我还以为楼主在那高喊『我用 go 协程实现了超牛逼的 async/await 语法』呢。就是个语法糖,没必要扯什么 go 哲学和架构设计吧。
    pastor
        19
    pastor  
       2022-09-02 16:57:14 +08:00
    @rrfeng 我只是劝你别研究这种吃力不讨好的东西了,如果你目前阶段的修为 get 不到,就忽略我说的吧。期待未来的某天或许你会恍然大悟
    pastor
        20
    pastor  
       2022-09-02 16:59:11 +08:00
    这玩意相比与 goroutine 是倒退,跟你帖子主题说自己搞的这个东西是否牛逼没关系。
    rrfeng
        21
    rrfeng  
    OP
       2022-09-02 17:24:22 +08:00
    @pastor 真搞不懂哪里来的优越感和这么喜欢批判别人
    pastor
        22
    pastor  
       2022-09-02 19:19:32 +08:00
    @rrfeng
    这不是优越感,只是我这个人说话比较实在并且直接,你听了可能会不舒服而已。
    至于为什么这样不懂得客气,是因为有过太多因为客气委婉、别人反倒以为自己没问题,所以我不想再客气了,有问题就尖酸刻薄地指出,至少对于技术本身,是中肯切实的

    批判跟优越感也没有直接关系。
    批判纯粹是因为你做的这个语法糖是一种倒退,如果没有其他人参与讨论我就不会来留言了,但是看到其他人也参与了讨论并且没有意识到这种语法糖是倒退,这就可能导致有更多人被误导。

    同样有一些其他人参考其他语言做一些对于 go 而言是倒退的东西,如果力所能及,我也都会献上一些建议不要这样做的刻薄说辞

    但我不只是空口乱喷,讲了一些点的,OP 要是能静下心来回到技术本身,对自己是有好处的

    OP 不要纠结我的说话方式,你就当我是个没礼貌的小学生无视我的不客气好了。对其他人也一样,每个人隔三差五总会遇到让自己不舒服的人,我们没法改变环境,但是能适应环境,只要不是切实伤害,自己内心强大就无所谓别人客气不客气了

    已经好些人说我刻薄之类的了,我自己也知道并且欣赏自己的刻薄。刻薄并不是什么坏事情,这世界,总是需要有一些刻薄的人的

    良药苦口,忠言逆耳,认真思考技术就行了,共勉
    pastor
        23
    pastor  
       2022-09-02 19:20:51 +08:00
    @rrfeng “真搞不懂哪里来的优越感和这么喜欢批判别人”

    补充一点,不是批判 OP 你这个人,是说这个语法糖这个实现
    rix
        24
    rix  
       2022-09-09 16:22:36 +08:00
    你需要 [tomb]( https://pkg.go.dev/gopkg.in/tomb.v2)

    可以看這篇介紹: http://blog.labix.org/2011/10/09/death-of-goroutines-under-control

    你似乎注意到了 Go Routine 的一大痛點,也就是沒有返回值和錯誤信息。

    Tomb 提供了一個描述異步過程生命週期的類型,可以很好地管理異步生命週期。

    但是 Tomb 仍然只是最基礎的生命週期描述符,它本身不具備多個異步生命週期的函數式組合。

    因此我寫了 [go-rx]( https://github.com/go-rx/rx) 你可以看看,是基於 Tomb 的,結合了 Rx ( Reactive Extension )異步函數式語言的異步生命週期管理和組合用的庫。你可以併發多個 Go Routine 然後合併它們的返回值和錯誤信息,可以使用函數式的語言進行 piping 等等。

    可以看我這篇文章瞭解更多: https://dev.to/rix/rx-with-go-generics-2fl6
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   1625 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 29ms · UTC 16:19 · PVG 00:19 · LAX 09:19 · JFK 12:19
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.