【问题标题】:How get result of running the goroutine?如何获得运行 goroutine 的结果?
【发布时间】:2021-08-21 08:50:30
【问题描述】:

在其他语言中,我可以同时运行多个任务并在相应的变量中获取每个任务的结果。

例如在 JS 中:

getApi1()
 .then( res => console.debug("Result 1: ", res) )

getApi2()
 .then( res => console.debug("Result 2: ", res) )

getApi3()
 .then( res => console.debug("Result 3: ", res) )

而且我确切地知道哪个函数的执行结果在哪个变量中。

在 Python asyncio 中也是如此:

task1 = asyncio.create_task(getApi1)
task2 = asyncio.create_task(getApi2)
result1 = await task1
result2 = await task2

我是 Go 语言的新手。所有指南都说要使用带有 goroutine 的通道。

但我不明白,当我从频道阅读时,如何确定哪个消息匹配哪个结果?

resultsChan := make(chan map)

go getApi1(resultsChan)
go getApi2(resultsChan)
go getApi3(resultsChan)

for {
 result, ok := <- resultsChan

 if ok == false {
  break
 
 } else {

  // HERE
  fmt.Println(result)  // How to understand which message the result of what API request?
 
 }

}

【问题讨论】:

  • make(chan map) 不会编译。请发布 MWE。要回答您的问题,如果您关心结果的来源,您应该使用适当的渠道元素类型(例如type Result struct { api string })。
  • @jub0bs,无论如何,如何将结果与我的 API 调用相匹配?
  • goroutines 不返回 results: 它们一直运行直到返回,但它们的返回类型始终是“什么都不返回”。所以 get 没有结果。
  • 这能回答你的问题吗? Catching return values from goroutines
  • resultsChan的元素类型是什么?如果它是一个结构,你能不能简单地添加一个字段来指示结果来自哪里?

标签: go


【解决方案1】:

如何理解哪个消息是什么API请求的结果?

如果同一通道要将所有getApiN 函数的结果传递给main,并且您想以编程方式确定每个结果的来源,您只需将专用字段添加到您的通道元素类型即可。下面,我已经声明了一个名为 Result 的自定义结构类型,其中包含一个名为 orig 的字段,正是为了这个目的。

package main

import (
    "fmt"
    "sync"
)

type Origin int

const (
    Unknown Origin = iota
    API1
    API2
    API3
)

type Result struct {
    orig Origin
    data string
}

func getApi1(c chan Result) {
    res := Result{
        orig: API1,
        data: "some value",
    }
    c <- res
}

func getApi2(c chan Result) {
    res := Result{
        orig: API2,
        data: "some value",
    }
    c <- res
}

func getApi3(c chan Result) {
    res := Result{
        orig: API3,
        data: "some value",
    }
    c <- res
}

func main() {
    results := make(chan Result, 3)
    var wg sync.WaitGroup
    wg.Add(3)
    go func() {
        defer wg.Done()
        getApi1(results)
    }()
    go func() {
        defer wg.Done()
        getApi2(results)
    }()
    go func() {
        defer wg.Done()
        getApi3(results)
    }()
    go func() {
        wg.Wait()
        close(results)
    }()
    for res := range results {
        fmt.Printf("%#v\n", res)
    }
}

(Playground)

可能的输出(结果的顺序不确定):

main.Result{orig:1, data:"some value"}
main.Result{orig:2, data:"some value"}
main.Result{orig:3, data:"some value"}

无论如何,我不会关注wic's suggestion;您的问题根本不是反射的好用例。 As Rob Pike puts it,

反射永远不会清晰。您在 Stack Overflow 上经常看到的另一件事是人们尝试使用 reflect 并想知道为什么它不起作用。它不起作用,因为它不适合你……很少有人应该玩反射。这是一个非常强大但非常难以使用的功能。 [...]

【讨论】:

  • 好点。赞成。但请注意twitter.com/Web3Coach/status/1400775870982262787
  • @VonC 不确定我理解;我无处增加计数器。
  • 对,我读你的答案太快了。我认为如果使用单个通道来识别每个结果,则需要增加一个 ID。
  • @VonC 没问题。感谢您的支持:)
【解决方案2】:

您可以为此使用chebyrash/promise(使用幕后频道)

var p1 = promise.Resolve(123)
var p2 = promise.Resolve("Hello, World")
var p3 = promise.Resolve([]string{"one", "two", "three"})

results, _ := promise.All(p1, p2, p3).Await()
fmt.Println(results)
// [123 Hello, World [one two three]]

如“Catching return values from goroutines”中所述:

这是 Go 创作者的设计选择。
有很多抽象/API 来表示异步 I/O 操作的价值 - promisefutureasync/awaitcallbackobservable

我上面提到的项目是一个允许获得“承诺”的组合示例。


要在本地实现相同的效果,您需要为每个预期结果提供一个渠道。
以“Use Go Channels as Promises and Async/Await”为例(来自Minh-Phuc Tran)和这个playground example

package main

import (
    "fmt"
    "math/rand"
    "time"
)

func longRunningTask() <-chan int32 {
    r := make(chan int32)

    go func() {
        defer close(r)
        
        // Simulate a workload.
        time.Sleep(time.Second * 3)
        r <- rand.Int31n(100)
    }()

    return r
}

func main() {
    aCh, bCh, cCh := longRunningTask(), longRunningTask(), longRunningTask()
    a, b, c := <-aCh, <-bCh, <-cCh
    
    fmt.Println(a, b, c)
}

来自 OP 的有趣的 cmets:

如果我需要同时运行 500 个 API 调用怎么办?我应该制作 500 个频道吗?

请参阅“Max number of goroutines”:500 个 goroutine(及其通道)什么都不是

如果我事先不知道 API 调用的次数怎么办?例如,我获取参数并通过它获取未知长度数组的 API 调用?

然后,假设我们正在讨论大量调用,您可以使用worker pool,如sophisticated example
您需要返回一个带有作业 ID 标记的结果,以便将其与期望特定结果的变量匹配。

【讨论】:

  • 这很有趣,谢谢。但我想知道如何在本地做到这一点。
  • @morfair 当然。我已经编辑了答案以说明(简单)本机实现。
  • 不,不,不...如果我需要同时运行 500 个 API 调用怎么办?我应该制作 500 个频道吗?如果我事先不知道 API 调用的次数怎么办?例如,我获取参数并通过它获取未知长度数组的 API 调用?
  • @morfair 1/ 它们很便宜,所以你可以拥有很多。 2/ 这取决于您的程序的性质:例如,您可以拥有一个频道池来管理 大量 批呼叫(我的意思是远远大于 500 个)。跨度>
  • 再一次,如何匹配来自频道池的结果?..
【解决方案3】:

另一种方法是使用 goroutine 而不是通道。这取决于您是否希望对回调进行一些同步。这个:

getApi1()
 .then( res => console.debug("Result 1: ", res))

getApi2()
 .then( res => console.debug("Result 2: ", res) )

getApi3()
 .then( res => console.debug("Result 3: ", res) )

会变成

go func(){
    res := getApi1()
    fmt.Println("Result 1: ", res)
}
go func(){
    res := getApi2()
    fmt.Println("Result 2: ", res)
}
go func(){
    res := getApi3()
    fmt.Println("Result 3: ", res)
}

【讨论】:

  • 我都想要。首先,我应该得到所有结果并一起处理。其次,我必须能够单独处理。
【解决方案4】:

最佳实践是为每个 goroutine 设置一个通道,并编写一个包含所有通道的 select 语句,但这听起来像是重复代码。因此,您需要这样做,但使用通用代码,Go 有一个名为 reflection 的强大包。如果您是 Go 新手,我建议您学习 reflection。因此,您可以应用此答案https://stackoverflow.com/a/19992525/10446155,代码如下所示:

package main

import (
    "fmt"
    "reflect"
)

func getApi1(c chan string) {
    c <- "value from Api1"
}

func getApi2(c chan string) {
    c <- "value from Api2"
}

func getApi3(c chan string) {
    c <- "value from Api3"
}

// Array with all channels
var responses []chan string

func init() {
    responses = make([]chan string, 3)
    for i := 0; i < 3; i++ {
        responses[i] = make(chan string)
    }
}

func main() {
    // Call each ApiFunction with different channel
    go getApi1(responses[0])
    go getApi2(responses[1])
    go getApi3(responses[2])

    // Generic select statement with reflection
    cases := make([]reflect.SelectCase, len(responses))
    for i, ch := range responses {
        cases[i] = reflect.SelectCase{
            Dir:  reflect.SelectRecv,
            Chan: reflect.ValueOf(ch),
        }
    }

    count := len(cases)
    for {
        chosen, value, ok := reflect.Select(cases)
        if !ok {
            panic(fmt.Sprintf("channel at index %d is closed", chosen))
        }
        fmt.Printf("getApi%d returns: %s\n", chosen+1, value.String())
        // Count each select, and break if all are made. If you don't 
        // do this, then your code enter in a deadlock condition
        count -= 1
        if count == 0 {
            break
        }
    }
}

注意:我想所有的 ApiFunctionrs 都会返回一个 string

【讨论】:

  • 是的,我建议为每个变量设置一个通道,...但并不顺利。
  • 我对使用反射和/或 init 的方法很常见...
猜你喜欢
  • 2023-02-09
  • 1970-01-01
  • 2018-12-27
  • 1970-01-01
  • 2020-10-13
  • 2020-08-22
  • 1970-01-01
  • 2012-04-02
  • 2014-04-25
相关资源
最近更新 更多