【问题标题】:Goroutines and Channels with Multiple Types多种类型的 Goroutines 和 Channels
【发布时间】:2016-08-20 14:21:22
【问题描述】:

我对 Go 比较陌生,并试图找出同时从 REST API 中提取信息的最佳方法。目的是对API 进行多次并发调用,每次调用都返回不同类型的数据。

我目前有:

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

但我想知道是否有更好的方法来写这个。

编辑:它仍然很丑,但将频道数量减少到了一个:

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

我仍然非常想要一种方法来执行此操作,因此我不必硬编码循环需要运行 6 次。我实际上尝试制作一个要运行的函数列表并以这种方式删除重复的go func 调用,但由于所有函数都有不同的返回类型,我得到了所有类型不匹配的错误,你不能通过使用来伪造它func(api) interface{} 那样只会造成运行时恐慌。

【问题讨论】:

  • 您可能过度使用频道。如果请求已经并发并且每个都返回单一类型的结果,为什么还需要更多的 goroutine 和通道来接收结果?
  • 这就是诀窍,每个返回不同类型的结果
  • 您可以在闭包中分配值:play.golang.org/p/Sak5QGCPZi,或者您可以按照下面的建议进行操作并使用通用接口{}来接收任何类型。
  • 关于那个的问题:我认为你必须使用通道而不是在 goroutine 内部进行直接分配。这不是真的吗?
  • 不,将闭包作为 goroutine 调度是非常惯用的。您始终必须确保永远不会有并发读取和写入,但是 WaitGroup 提供了同步。

标签: go channel goroutine


【解决方案1】:

当我看到这一点时,我认为我们可能将分配与完成混为一谈,从而为每种类型创建一个通道。

为每个类型创建一个闭包用于分配和一个通道来管理完成可能更简单。

示例:

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

Go 作者预见到了这个用例,并提供了 sync.WaitGroup,这使得这更清晰sync.WaitGroup Docs 下面是取代通道同步的花哨的原子操作。

示例:

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

我希望这会有所帮助。

【讨论】:

  • 我认为这是迄今为止我见过的最完整的例子。我最终使用了 WaitGroup 示例,它运行良好。
  • 我喜欢在这里使用等待组。有没有办法少重复自己呢?就像创建一片函数然后循环它们一样?
【解决方案2】:

作为chan interface{] 方法的替代方法,您可以考虑创建一个信封对象(在下面的示例中为APIObject)以指向您希望发送的真实值。它带来了内存开销,但也增加了类型安全性。 (非常感谢权衡这两种方法的评论和意见。)

https://play.golang.org/p/1KIZ7Qfg43

package main

import (
    "fmt"
)

type Service struct{ Name string }
type ServicePlan struct{ Name string }
type Organization struct{ Name string }

type APIObject struct {
    Service      *Service
    ServicePlan  *ServicePlan
    Organization *Organization
}

func main() {
    objs := make(chan APIObject)

    go func() {
        objs <- APIObject{Service: &Service{"Service-1"}}
        objs <- APIObject{ServicePlan: &ServicePlan{"ServicePlan-1"}}
        objs <- APIObject{Organization: &Organization{"Organization-1"}}
        close(objs)
    }()

    for obj := range objs {
        if obj.Service != nil {
            fmt.Printf("Service: %v\n", obj.Service)
        }
        if obj.ServicePlan != nil {
            fmt.Printf("ServicePlan: %v\n", obj.ServicePlan)
        }
        if obj.Organization != nil {
            fmt.Printf("Organization: %v\n", obj.Organization)
        }
    }
}

【讨论】:

    【解决方案3】:

    您的方法非常有意义,几乎没有免责声明。最后,您会按顺序从频道接收

    services := <- c1
    servicePlans := <- c2
    serviceInstances := <- c3
    serviceBindings := <- c4
    

    但这会使您的代码同步,因为

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

    只要对面可以读取,就不能写入 c2,并且在services := &lt;- c1 执行之前不会写入。因此,尽管在 goroutines 中,代码仍将按顺序执行。使代码更加异步的最简单方法是至少提供缓冲通道

    c1 := make(chan map[string]Service, 1) //buffered channel with capacity one
    

    允许 c1

    【讨论】:

    • 你是对的。使用多个渠道会带来几个可能的问题。我修改了我的答案以包含一个运行良好的单通道版本,但我想我现在要使用 WaitGroup 版本。感谢您指出同步问题!
    【解决方案4】:

    您可能会发现创建interface{} 类型的单个通道更容易,这将允许您发送任何值。然后,在接收端,您可以对特定类型执行类型断言:

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

    【讨论】:

    • 我曾考虑过类似的方法,但我看到的问题是这只等待一个响应返回,而不是所有响应。我假设 switch 需要处于某种 for 循环中,并且需要知道该循环何时应该中断。
    • @RossPeoples 如果您需要接收多个值,可以将接收/切换代码放在for{} 循环中。
    • @RossPeoples,不需要在 for 周围加上开关(除非你愿意)。 golang.org/ref/spec#Break_statements:“break”语句会终止同一个函数中最里面的“for”、“switch”或“select”语句的执行。
    • 这是我遇到问题的 for{} 循环。我没有办法告诉它跳出循环。除非您关闭通道,否则在通道上使用范围不起作用,而且我认为在此示例中没有合适的位置。
    猜你喜欢
    • 2015-12-26
    • 2021-01-09
    • 2021-08-07
    • 1970-01-01
    • 2019-10-01
    • 2011-06-25
    • 2016-09-20
    • 1970-01-01
    • 2016-05-02
    相关资源
    最近更新 更多