Golang中Goroutine生成的最佳实践
Golang中Goroutine生成的最佳实践 我想了解创建 goroutine 的最佳实践,是在调用者函数中创建,还是在被调用者函数中创建?
在调用者函数中创建可以明确这是一个后台任务/goroutine。但在某些场景下,我们需要在被调用者函数中创建,以便更好地控制 goroutine(例如使用 Channel/WaitGroup)。
我想知道大家是否有遵循某种指导原则来做这个决定。
示例
type DataCollector struct {
...
}
func (d *DataCollector) Start() {
// 或者也可以在这里创建 goroutine
// go func() {
// ...
// }
...
}
func main() {
dataCollector := DataCollector{}
go dataCollector.Start()
...
}
更多关于Golang中Goroutine生成的最佳实践的实战教程也可以访问 https://www.itying.com/category-94-b0.html
另一个经验法则是,在启动一个 goroutine 之前,要清楚它将如何停止。……而这正是我的问题所在。
我正在构建一个模仿 Pick 的数据库,它提供了一种编程语言。每个用户/会话都作为一个 goroutine 运行。使用 context 的方法会有效,除非遇到一个预期会长时间运行、超过一般 context 过期时间的程序。通常,你可能会通过一个 SELECT 语句来检查管理员是否在你的 QUIT 通道上发送了终止请求。但是,如果一个程序卡在无限循环中(例如,LABEL: GO TO LABEL),它就永远不会返回到执行检查的代码段。
至少这里有一个变通方法。只有在向后跳转时才会出现无限循环。方法调用也可以间接地实现同样效果。所以,至少你可以限制需要检查的位置和次数。
到目前为止,这是一段有趣的旅程。
这是一个很好的问题。我认为这取决于 Start 函数的行为以及它将如何被使用。有一个通用原则——除非必要,否则不要使用通道。
我喜欢在 main 函数中描述的那个选项。如果只是为了让 Start 并发运行,那么把决定权留给调用者。但是……
如果你想让 Start 函数可以被取消,那么你应该在其中传递上下文并在那里处理它。
func (d *DataCollector) Start(ctx context.Context) {
go func() {
doSomething(ctx)
c <- struct{}
}()
select {
case <-ctx.Done(): // cancelled
return
case <-c:
// everything went OK
}
}
这取决于函数内部的逻辑。没有具体的上下文,很难给出一个通用的答案。例如,如果你只想启动收集器而不期望它们很快完成,那么采用 main() 中的方法即可。如果 Start 函数有更复杂的逻辑,例如调用一些 I/O 操作,你应该在 goroutine 中运行这些任务。
在Golang中,goroutine创建位置的选择确实需要根据具体场景权衡。以下是两种方式的对比和示例:
1. 调用者创建(显式控制)
调用者明确知道自己在启动并发任务,便于生命周期管理。
// 调用者创建goroutine
func main() {
var wg sync.WaitGroup
collector := NewDataCollector()
// 调用者明确控制并发
wg.Add(1)
go func() {
defer wg.Done()
collector.Start()
}()
// 可以启动多个并等待
wg.Wait()
}
2. 被调用者创建(封装并发)
被调用函数内部处理并发细节,对外提供同步接口。
type DataCollector struct {
dataChan chan string
done chan struct{}
}
// 被调用者内部创建goroutine
func (d *DataCollector) Start() {
go d.collect()
go d.process()
}
func (d *DataCollector) collect() {
for {
select {
case <-d.done:
return
case data := <-d.dataChan:
// 处理数据
_ = data
}
}
}
func (d *DataCollector) process() {
// 处理逻辑
}
3. 实际场景示例
场景A:需要等待结果
// 调用者创建,便于同步
func ProcessBatch(items []string) []Result {
var wg sync.WaitGroup
results := make([]Result, len(items))
for i, item := range items {
wg.Add(1)
go func(idx int, data string) {
defer wg.Done()
results[idx] = processItem(data)
}(i, item)
}
wg.Wait()
return results
}
场景B:长期运行的服务
// 被调用者创建,封装实现细节
type Server struct {
listener net.Listener
stop chan struct{}
}
func (s *Server) Start() error {
// 内部启动多个goroutine
go s.acceptConnections()
go s.monitorMetrics()
go s.cleanupSessions()
return nil
}
func (s *Server) acceptConnections() {
for {
select {
case <-s.stop:
return
default:
conn, err := s.listener.Accept()
if err != nil {
continue
}
go s.handleConnection(conn) // 每个连接一个goroutine
}
}
}
4. 指导原则
- API设计角度:如果函数需要返回结果或可能被阻塞,让调用者决定是否并发
- 封装角度:如果并发是实现细节,应在被调用者内部处理
- 控制角度:需要协调多个goroutine时,调用者创建更合适
- 错误处理:被调用者创建时需考虑panic恢复和错误传递
// 混合模式:提供同步和异步两种接口
type Processor struct {
// ...
}
// 同步版本
func (p *Processor) ProcessSync(data Data) (Result, error) {
return p.process(data)
}
// 异步版本(内部创建goroutine)
func (p *Processor) ProcessAsync(data Data) <-chan Result {
resultChan := make(chan Result, 1)
go func() {
defer close(resultChan)
result, err := p.process(data)
if err == nil {
resultChan <- result
}
}()
return resultChan
}
选择哪种方式取决于具体需求:调用者创建提供更明确的控制,被调用者创建提供更好的封装性。在实际项目中,这两种模式经常结合使用。


