Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Goroutines and Channels with Multiple Types

I'm relatively new to Go and trying to figure out the best way to concurrently pull information from a REST API. The intent is to make multiple concurrent calls to an API, with each call returning a different type of data.

I currently have:

s := NewClient()
c1 := make(chan map[string]Service)
c2 := make(chan map[string]ServicePlan)
c3 := make(chan map[string]ServiceInstance)
c4 := make(chan map[string]ServiceBinding)
c5 := make(chan map[string]Organization)
c6 := make(chan map[string]Space)

go func() {
    c1 <- GetServices(s)
}()

go func() {
    c2 <- GetServicePlans(s)
}()

go func() {
    c3 <- GetServiceInstances(s)
}()

go func() {
    c4 <- GetServiceBindings(s)
}()

go func() {
    c5 <- GetOrganizations(s)
}()

go func() {
    c6 <- GetSpaces(s)
}()

services := <- c1
servicePlans := <- c2
serviceInstances := <- c3
serviceBindings := <- c4
orgs := <- c5
spaces := <- c6
// stitch all the data together later

but I was wondering if there was a better way to write this.

EDIT: It's still ugly, but reduced the number of channels to one:

c := make(chan interface{})

var (
    services     map[string]Service
    servicePlans     map[string]ServicePlan
    serviceInstances map[string]ServiceInstance
    serviceBindings  map[string]ServiceBinding
    orgs         map[string]Organization
    spaces       map[string]Space
)

go func() {
    c <- GetServices(s)
}()

go func() {
    c <- GetServicePlans(s)
}()

go func() {
    c <- GetServiceInstances(s)
}()

go func() {
    c <- GetServiceBindings(s)
}()

go func() {
    c <- GetOrganizations(s)
}()

go func() {
    c <- GetSpaces(s)
}()

for i := 0; i < 6; i++ {
    v := <-c
    switch v := v.(type) {
    case map[string]Service:
        services = v
    case map[string]ServicePlan:
        servicePlans = v
    case map[string]ServiceInstance:
        serviceInstances = v
    case map[string]ServiceBinding:
        serviceBindings = v
    case map[string]Organization:
        orgs = v
    case map[string]Space:
        spaces = v
    }
}

I would still really like a way to do this so I don't have to hard-code that the loop needs to run 6 times. I actually tried make a list of functions to run and doing it that way to remove the repetitive go func calls, but since all the functions have different return types, I got all type mismatch errors, and you can't fake it by using func(api) interface{} either as that just creates a runtime panic.

like image 967
Ross Peoples Avatar asked Apr 26 '16 16:04

Ross Peoples


Video Answer


2 Answers

When I see this, I think we may be conflating assignment with completion, thus creating one channel per type.

It may be simpler to create one closure per type for assignment and a single channel to manage completion.

example:

s := NewClient()
c := make(chan bool)
// I don't really know the types here
var services services
var servicePlans servicePlans
var serviceInstances serviceInstances
var serviceBindings serviceInstances
var orgs orgs
var spaces spaces

go func() {
    service = GetServices(s)
    c <- true
}()

go func() {
    servicePlans = GetServicePlans(s)
    c <- true
}()

go func() {
    serviceInstances = GetServiceInstances(s)
    c <- true
}()

go func() {
    serviceBindings = GetServiceBindings(s)
    c <- true
}()

go func() {
    orgs = GetOrganizations(s)
    c <- true
}()

go func() {
    spaces = GetSpaces(s)
    c <- true
}()

for i = 0; i < 6; i++ {
    <-c
}
// stitch all the data together later

The Go authors anticipated this use-case, and provide the sync.WaitGroup which makes this a little clearer sync.WaitGroup Docs Underneath it is fancy atomic operations which replace the channel synchronization.

example:

s := NewClient()
// again, not sure of the types here
var services services
var servicePlans servicePlans
var serviceInstances serviceInstances
var serviceBindings serviceInstances
var orgs orgs
var spaces spaces

var wg sync.WaitGroup
wg.Add(6)

go func() {
    service = GetServices(s)
    wg.Done()
}()

go func() {
    servicePlans = GetServicePlans(s)
    wg.Done()
}()

go func() {
    serviceInstances = GetServiceInstances(s)
    wg.Done()
}()

go func() {
    serviceBindings = GetServiceBindings(s)
    wg.Done()
}()

go func() {
    orgs = GetOrganizations(s)
    wg.Done()
}()

go func() {
    spaces = GetSpaces(s)
    wg.Done()
}()

// blocks until all six complete
wg.Wait()
// stitch all the data together later

I hope this is helps.

like image 56
foo Avatar answered Jan 01 '23 07:01

foo


You might find it easier to create a single channel of type interface{} which will allow you to send any value. Then, on the receiving end, you can perform a type assertion for a specific type:

c := make(chan interface{})

/* Sending: */
c <- 42
c <- "test"
c <- &ServicePlan{}

/* Receiving */
something := <-c
switch v := something.(type) {
case int:          // do something with v as an int
case string:       // do something with v as a string
case *ServicePlan: // do something with v as an instance pointer
default:
}
like image 20
Nathan Osman Avatar answered Jan 01 '23 08:01

Nathan Osman