How get result of running the goroutine?

Viewed 530

In other languages, I can run several tasks in concurrently and get the result of each in corresponding variable.

For example in 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) )

And I know exactly in which variable the result of the execution of which function.

The same is in Python asyncio:

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

I'm new in Go lang. All guides say to use channels with goroutines.

But I don't understand, when I read from the channel, how to be sure which message matches which result?

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?
 
 }

}

4 Answers

How to understand which message the result of what API request?

If the same channel is to communicate the results from all your getApiN functions to main and you want to programmatically determine where each result came from, you can simply add a dedicated field to your channel element type. Below, I've declared a custom struct type named Result with a field named orig for precisely that purpose.

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)

Possible output (the order of results isn't deterministic):

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

At any rate, I would not follow wic's suggestion; your problem is simply not a good use case for reflection. As Rob Pike puts it,

Reflection is never clear. Another thing you see on Stack Overflow a lot is people trying to use reflect and wondering why it doesn't work. It doesn't work because it's not for you... Very, very few people should be playing with reflection. It's a very powerful but very difficult-to-use feature. [...]

You could use chebyrash/promise for that (which uses channel behind the scene)

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]]

As explained in "Catching return values from goroutines":

It is a design choice by Go creators.
There's a whole lot of abstractions/APIs to represent the value of async I/O operations - promise, future, async/await, callback, observable, etc

The project I mention above is an example of composition which allows to get "promises".


To achieve the same natively, you would need one channel per expected result.
See as an example "Use Go Channels as Promises and Async/Await" (from Minh-Phuc Tran), and this 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)
}

Interesting comments from the OP:

What if I need run 500 API calls concurrently? Should I make 500 channels?

See "Max number of goroutines": 500 goroutines (and their channels) is nothing

What if I don't know in advance count of API calls? For example, I get params and get API calls over it array unknown length?

Then, assuming we are talking about a large number of calls, you can use a worker pool, as in this more sophisticated example.
You would need to return a result tagged with a job id in order to match it with the variable which expect a specific result.

An other way to do is to use goroutine rather than channels. It depends whether you want to have some synchronization on the callbacks or not. This:

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

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

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

Would become

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)
}

The best practice is to have a channel for each goroutine and make a select statement containing all channels, but it sounds like much-repeated code. So, you need to do that but with a generic code, Go have a powerful package called reflection. If you are new to Go, I recommend you to learn reflection. So, you can apply this answer https://stackoverflow.com/a/19992525/10446155 and the code looks like this:

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
        }
    }
}

Note: I suppose all ApiFunctionrs returns a string

Related