【问题标题】:How to return first http response to answer如何返回第一个http响应以回答
【发布时间】:2020-02-14 01:51:36
【问题描述】:

我使用多个 goroutine 来运行任务,当其中一个完成时,返回并关闭通道,这将导致恐慌:在关闭的通道上发送

见代码:

func fetch(urls []string) *http.Response {
    ch := make(chan *http.Response)
    defer close(ch)
    for _, url := range urls {
        go func() {
            resp, err := http.Get(url)
            if err == nil {
                ch <- resp
            }
        }()
    }
    return <-ch
}

不关闭频道是没有问题的,但是我觉得不是很好,有什么优雅的解决方案吗?

感谢所有答案,这是我的最终代码:

func fetch(urls []string) *http.Response {
    var wg sync.WaitGroup
    ch := make(chan *http.Response)
    wg.Add(len(urls))
    for _, url := range urls {
        go func(url string) {
            defer wg.Done()
            resp, err := http.Get(url)
            if err == nil {
                ch <- resp
            }
        }(url)
    }
    go func() {
        wg.Wait()
        close(ch)
    }()
    return <-ch
}

【问题讨论】:

  • 问题是 fetch() 在你的 goroutine 完成之前返回,当它返回时,defer close(ch) 执行。你可能需要一个等待组。
  • return &lt;-ch 也没有意义......它只会返回一个值(任何 goroutine 返回的第一个值),但您正在获取多个值。我不确定你的目标是什么,但你的代码没有意义。
  • 这可能是有道理的,假设您只想要第一个响应。不过我也觉得很奇怪,很可能应该返回频道。
  • @Clément:这方面可能是有道理的,但是你需要(优雅地)清理其他 goroutine,这还没有完成。

标签: go


【解决方案1】:

在这个版本中,ch 频道有足够的空间,因此如果相应的频道阅读器不存在,例程可以推送到它而不会阻塞。

package main

import (
    "fmt"
    "net/http"
    "sync"
)

func main() {
    urls := []string{"", "", ""}
    res := fetch(urls)
    fmt.Println(res)
}
func fetch(urls []string) *http.Response {
    var wg sync.WaitGroup
    ch := make(chan *http.Response, len(urls))

    for _, url := range urls {
        wg.Add(1)
        url := url
        go func() {
            defer wg.Done()
            req, err := http.NewRequest(http.MethodGet, url, nil)
            if err != nil {
                return
            }
            resp, err := http.DefaultClient.Do(req)
            if err != nil {
                return
            }
            if resp != nil {
                ch <- resp // no need to test the context, ch has rooms for this push to happen anyways.
            }
        }()
    }

    go func() {
        wg.Wait()
        close(ch)
    }()

    return <-ch
}

https://play.golang.org/p/5KUeaUS2FLg

此版本说明了在取消请求中附加了context 的实现。

package main

import (
    "context"
    "fmt"
    "net/http"
    "sync"
)

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()
    cancel()
    urls := []string{"", "", ""}
    res := fetch(ctx, urls)
    fmt.Println(res)
}

func fetch(ctx context.Context, urls []string) *http.Response {
    var wg sync.WaitGroup
    ch := make(chan *http.Response, len(urls))
    for _, url := range urls {
        if ctx.Err() != nil {
            break // break asap.
        }
        wg.Add(1)
        url := url
        go func() {
            defer wg.Done()
            req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
            if err != nil {
                return
            }
            resp, err := http.DefaultClient.Do(req)
            if err != nil {
                return
            }
            if resp != nil {
                ch <- resp // no need to test the context, ch has rooms for this push to happen anyways.
            }
        }()
    }
    go func() {
        wg.Wait()
        close(ch)
    }()
    return <-ch
}

https://play.golang.org/p/QUOReYrWqDp

友情提醒,不要太聪明,用sync.WaitGroup,用最简单的逻辑写流程,让它流动,直到你可以安全地close那个频道。

【讨论】:

  • 我同意这是正确的,并且在减少一个 goroutine 的意义上更简单。虽然: 1. 您的wg.Wait() 语句可能在任何wg.Add(1) 执行之前被读取,这会导致恐慌。您可以在 for 语句之后移动该 goroutine 来修复。 2.你最后一个select永远不会使用上下文案例,你可以删除这个select
  • 如果在 Add 之前调用 Wait(),它会退出,它不会恐慌。但是,可能会在关闭的通道上写入。我不同意你关于 2 的看法。
  • 是的,在封闭的频道上写会惊慌失措……对于 2,我看不出你如何选择“完成”的情况,因为 cancel 只会在 @ 之后被调用987654336@声明。
  • 既然缓冲通道在函数作用域内,那么其他 N-1 个响应(在函数返回后不会被读取)可以在以后进行垃圾回收吗?
  • @TobiasPfandzelter 是的,stackoverflow.com/a/36613932/4466350
【解决方案2】:

如果您的目标是只读取一个结果,然后取消其他请求,请尝试以下操作:

func fetch(urls []string) *http.Response {
    ch := make(chan *http.Response)
    defer close(ch)
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()
    for _, url := range urls {
        go func(ctx context.Context, url string) {
            req, _ := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
            resp, err := http.Do(req)
            if err == nil {
                select {
                case ch <- resp:
                case <- ctx.Done():
                }
            }
        }(ctx, url)
    }
    return <-ch
}

这使用了一个可取消的上下文,因此一旦返回第一个结果,剩余的 http 请求就会发出中止信号。


注意:您的代码有一个我已在上面修复的错误:

func _, url := range urls {
    go func() {
        http.Do(url) // `url` is changed here on each iteration through the for loop, meaning you will not be calling the url you expect
    }()
}

通过将 url 传递给 goroutine 函数来解决此问题,而不是使用闭包:

func _, url := range urls {
    go func(url string) {
        http.Do(url) // `url` is now safe
    }(url)
}

相关帖子:Why golang don't iterate correctly in my for loop with range?

【讨论】:

  • 我也在考虑这个问题,但我不相信这是完全安全的。如果两个请求在任何一个都可以到达select 语句时完成怎么办?或者如果fetch 消耗通道中的值并关闭通道,就在请求完成的时候并且在它到达select 语句之前呢?
  • @Clément:cancel 应该发生在 close(ch) 之前以防止出现这种情况,你是对的。我会更新答案。
  • 注意,在这种实现中出错的一个简单方法是引入一个合法但被忽视的早期返回,以防止http 调用返回的非零错误,if err != nil {return}。这是假设的,但让我们考虑一些维护和不完整的测试用例套件,如果所有请求都失败,函数将死锁,因为永远不会写入 ch
  • 如果所有请求都失败,则存在一个微妙的错误,永远不会写入 ch。此外,它可能会因write on a closed channel 而恐慌,因为defer close(ch) 可能在这些都完成ch &lt;- resp 之前发生。这样的恐慌会发生在 select 中吗?我需要仔细检查....
  • 这很令人费解,我不得不引入睡眠(在例程完成之前返回)并禁用错误(因为我猜该播放不允许传出请求)以使关闭通道上的恐慌发生。 play.golang.org/p/7BXBRBVjaUV
【解决方案3】:

你关闭频道太快了,所以你会看到这个错误,
最好只在你不再向频道写入任何内容时关闭频道,为此你可以使用sync.WaitGroup,如下所示:

package main

import (
    "fmt"
    "net/http"
    "sync"
)

func main() {
    ch := fetch([]string{"http://github.com/cn007b", "http://github.com/thepkg"})
    fmt.Println("\n", <-ch)
    fmt.Println("\n", <-ch)
}

func fetch(urls []string) chan *http.Response {
    ch := make(chan *http.Response, len(urls))
    wg := sync.WaitGroup{}
    wg.Add(len(urls))
    for _, url := range urls {
        go func() {
            defer wg.Done()
            resp, err := http.Get(url)
            if err == nil {
                ch <- resp
            }
        }()
    }
    go func() {
        wg.Wait()
        close(ch)
    }()
    return ch
}

此外,为了向切片提供响应,您可以执行以下操作:

func fetch2(urls []string) (result []*http.Response) {
    ch := make(chan *http.Response, len(urls))
    wg := sync.WaitGroup{}
    wg.Add(len(urls))
    for _, url := range urls {
        go func() {
            defer wg.Done()
            resp, err := http.Get(url)
            if err == nil {
                ch <- resp
            }
        }()
    }
    wg.Wait()
    close(ch)
    for v := range ch {
        result = append(result, v)
    }
    return result
}

【讨论】:

  • tks,但似乎缺少代码“wg.Done()”。
  • @liwei2633 在 goroutine 里面。
  • 对不起,我只是没看到,我瞎了:)
  • 还将频道更新为ch := make(chan *http.Response, len(urls)),所以现在它是非阻塞的。
【解决方案4】:

您的代码在收到第一个响应后返回。然后关闭通道,让其他 go 例程在关闭的通道上发送。

与返回第一个响应相比,返回一个响应数组可能更合适,按照与 url 相同的长度排序。

由于 http 请求可能出错,因此也应该返回一个错误数组。

package main

import (
    "fmt"
    "net/http"
)

func main() {
    fmt.Println(fetch([]string{
        "https://google.com",
        "https://stackoverflow.com",
        "https://passkit.com",
    }))
}

type response struct {
    key      int
    response *http.Response
    err      error
}

func fetch(urls []string) ([]*http.Response, []error) {
    ch := make(chan response)
    defer close(ch)
    for k, url := range urls {
        go func(k int, url string) {
            r, err := http.Get(url)
            resp := response {
                key:      k,
                response: r,
                err:      err,
            }
            ch <- resp
        }(k, url)
    }

    resp := make([]*http.Response, len(urls))
    respErrors := make([]error, len(urls))

    for range urls {
        r := <-ch
        resp[r.key] = r.response
        respErrors[r.key] = r.err
    }
    return resp[:], respErrors[:]
}

playground

【讨论】:

  • 这个策略不是return the valid first http response,但它很简洁,不要误会我的意思。
【解决方案5】:

你可以添加两个 goroutine:

  1. 接收所有请求,发送第一个要返回的请求并丢弃以下请求。当 WaitGroup 完成时,它会关闭您的第一个频道。
  2. 等待 WaitGroup 并发送关闭第一个通道的信号。
func fetch(urls []string) *http.Response {
    var wg sync.WaitGroup
    ch := make(chan *http.Response)
    for _, url := range urls {
        wg.Add(1)
        go func(url string) {
            resp, err := http.Get(url)          
            if err == nil {
                ch <- resp:
            }
            wg.Done()
        }(url)
    }
    done := make(chan interface{})
    go func(){
        wg.Wait()
        done <- interface{}{}
        close(done)
    }

    out := make(chan *http.Response)
    defer close(out)
    go func(){
        first = true
        for {
            select {
            case r <- ch:
                if first {
                    first = false
                    out <- r
                }
            case <-done:
                close(ch)
                return
            }
        }
    }()

    return <-out
}

这应该是安全的……也许吧。

【讨论】:

  • 无用的复杂。异步序列WaitGroup.Wait()+close(chan) 是安全的。
  • 我也这么认为,但我只是想找到解决安全问题的方法。您的解决方案既解决了安全问题,也解决了复杂性问题。
【解决方案6】:

您最后推荐的代码仅在您的至少一个调用成功时才有效。如果您为每个 HTTP GET 生成错误,您的函数将永远阻塞。您可以添加第二个频道来通知您通话已完成:

func fetch(urls []string) *http.Response {
    var wg sync.WaitGroup
    ch := make(chan *http.Response, len(urls))
    done := make(chan struct{})
    wg.Add(len(urls))
    for _, url := range urls {
        go func(url string) {
            defer wg.Done()
            resp, err := http.Get(url)
            // only put a response into the channel if we didn't get an error
            if err == nil {
                ch <- resp
            }
        }(url)
    }
    go func() {
        wg.Wait()
        // inform main routine that all calls have exited
        done <- struct{}{}
        close(ch)
    }()

    // return either the first response or nil
    select {
        case r := <-ch:
        return r
        case <-done:
        break
   }

   // you can do additional error handling here
   return nil
}

【讨论】:

  • 是的。在您的情况下,请考虑在wg.Wait 之后仅使用close(ch)。关闭通道上的读取返回零值 (stackoverflow.com/a/29269960/4466350)。读取 nil 通道将永远阻塞 (medium.com/justforfunc/…)。虽然,一个小评论使它成为一个很好的答案。看不懂The code you recommended at the end only works if at least one of your calls succeeds.,可以直接链接回答(有分享按钮)。 done 通道也是多余的。
  • play.golang.org/p/WKhnF8ghXVJ 但这是一个很好的收获
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-01-11
  • 1970-01-01
  • 1970-01-01
  • 2019-06-25
  • 2018-08-06
相关资源
最近更新 更多