问题

golang如何令程序在go关键字修饰的方法(函数) 执行完成后 再结束?

回答
在 Go 语言中,如果你想让程序在 `go` 关键字修饰的函数(通常称为 Goroutine)执行完成后再结束,你需要掌握 Goroutine 的同步和通信机制。这就像是给你的主程序一个信号,告诉它:“嘿,我这边还有一个正在忙活的家伙,等他忙完了,你再走。”

下面我将详细讲解实现这一目标的几种常用方法,并尽量用通俗易懂的方式来阐述,让你能够透彻理解。

为什么需要等待 Goroutine 完成?

默认情况下,主程序(也就是你直接运行的 `main` 函数)是独立于它启动的 Goroutine 运行的。一旦 `main` 函数执行完毕,整个程序就会退出,无论它启动了多少个 Goroutine。这就像你在一个会议室里,自己说完话就走了,而没有等其他人发言完毕。

所以,当你有一个需要在后台运行的任务(比如网络请求、文件处理、并发计算等),并且主程序需要确保这个任务完成后才能安全退出时,你就需要引入等待机制。

核心同步机制:`sync.WaitGroup`

在 Go 的标准库中,`sync.WaitGroup` 是最常用且最适合解决这个问题的工具。你可以把它想象成一个计数器,用于跟踪一组 Goroutine 的完成情况。

`sync.WaitGroup` 的工作原理:

1. 初始化 ( `sync.WaitGroup` ):
你在主程序中创建一个 `sync.WaitGroup` 实例。
2. 增加计数器 ( `wg.Add(n)` ):
在你启动每一个需要等待的 Goroutine 之前,调用 `wg.Add(1)`。这就像告诉 `WaitGroup`:“我要开始一个 Goroutine 了,请把计数器加一。”
3. 标记完成 ( `wg.Done()` ):
在每一个 Goroutine 即将结束时,调用 `wg.Done()`。这就像告诉 `WaitGroup`:“我这个 Goroutine 已经完成任务了,请把计数器减一。”
4. 等待 ( `wg.Wait()` ):
在主程序中,当你需要等待所有 Goroutine 完成时,调用 `wg.Wait()`。这个调用会阻塞(暂停)主程序的执行,直到 `WaitGroup` 的计数器归零(即所有通过 `wg.Add(1)` 添加的 Goroutine 都调用了 `wg.Done()`)。

代码示例:

```go
package main

import (
"fmt"
"sync"
"time"
)

func worker(id int, wg sync.WaitGroup) {
// defer 关键字确保 wg.Done() 无论函数如何退出都会被调用
defer wg.Done()

fmt.Printf("Worker %d: 开始工作... ", id)

// 模拟一些工作
time.Sleep(time.Second 2)

fmt.Printf("Worker %d: 工作完成! ", id)
}

func main() {
var wg sync.WaitGroup // 1. 初始化 WaitGroup

numWorkers := 3 // 我们要启动 3 个 Goroutine

for i := 1; i <= numWorkers; i++ {
wg.Add(1) // 2. 每次启动一个 Goroutine 前,计数器加一
go worker(i, &wg) // 启动 Goroutine,并传递 WaitGroup 的指针
}

fmt.Println("主程序: 所有工作者 Goroutine 已启动。等待它们完成...")

wg.Wait() // 4. 等待所有 Goroutine 完成(计数器归零)

fmt.Println("主程序: 所有工作者 Goroutine 都已完成。程序即将退出。")
}
```

解释上面的代码:

`var wg sync.WaitGroup`: 创建了一个 `WaitGroup` 变量。
`wg.Add(1)`: 在 `go worker(i, &wg)` 之前调用,告诉 `WaitGroup` 我们要启动一个 Goroutine。
`go worker(i, &wg)`: 启动了一个新的 Goroutine,它会执行 `worker` 函数。注意我们将 `wg` 的地址(指针)传递给了 `worker` 函数,这样 `worker` 函数才能在内部调用 `wg.Done()`。
`defer wg.Done()`: 在 `worker` 函数的顶部使用 `defer` 是非常关键的。`defer` 会将 `wg.Done()` 的调用推迟到当前函数(`worker`)返回之前执行。这保证了无论 `worker` 函数是否因为错误而提前退出,`wg.Done()` 都会被调用,从而防止程序永远阻塞在 `wg.Wait()`。
`wg.Wait()`: 在 `main` 函数的末尾,调用 `wg.Wait()`。这会暂停 `main` 函数的执行,直到 `wg` 的内部计数器降到 0。

当你运行这段代码时,你会看到类似这样的输出:

```
主程序: 所有工作者 Goroutine 已启动。等待它们完成...
Worker 1: 开始工作...
Worker 2: 开始工作...
Worker 3: 开始工作...
Worker 1: 工作完成!
Worker 2: 工作完成!
Worker 3: 工作完成!
主程序: 所有工作者 Goroutine 都已完成。程序即将退出。
```

主程序会在所有 Worker 都打印出“工作完成!”之后才打印最后一条消息并退出。

另一种方式:使用 Channel 进行信号通知

除了 `sync.WaitGroup`,你还可以使用 channel 来实现 Goroutine 的完成通知。Channel 是 Go 中 Goroutine 之间进行通信和同步的主要方式。

你可以创建一个无缓冲的 channel,让每个 Goroutine 在完成时向这个 channel 发送一个信号(比如一个空结构体 `struct{}`)。主程序则会等待从 channel 中接收到足够数量的信号。

代码示例:

```go
package main

import (
"fmt"
"time"
)

func workerWithChannel(id int, done chan struct{}) {
fmt.Printf("Worker %d: 开始工作... ", id)

// 模拟一些工作
time.Sleep(time.Second 2)

fmt.Printf("Worker %d: 工作完成! ", id)
done < struct{}{} // 发送完成信号到 channel
}

func main() {
numWorkers := 3
// 创建一个无缓冲的 channel,用于接收完成信号
done := make(chan struct{}, numWorkers) // 缓冲区大小设为 numWorkers 可以防止 Goroutine 因 channel 阻塞

for i := 1; i <= numWorkers; i++ {
go workerWithChannel(i, done) // 启动 Goroutine,传递 channel
}

fmt.Println("主程序: 所有工作者 Goroutine 已启动。等待它们完成...")

// 等待接收 numWorkers 个完成信号
for i := 0; i < numWorkers; i++ {
}

fmt.Println("主程序: 所有工作者 Goroutine 都已完成。程序即将退出。")
}
```

解释上面的代码:

`done := make(chan struct{}, numWorkers)`: 创建了一个 channel,类型是 `struct{}`(一个不占内存的空结构体,常用于表示信号),并设置了容量为 `numWorkers`。这个容量很重要:如果 channel 是无缓冲的(容量为 0),当 Goroutine 尝试发送数据而主程序还没有准备好接收时,Goroutine 会被阻塞。设置容量可以避免这种情况,让 Goroutine 能够立即发送信号。
`done < struct{}{}`: 在 `workerWithChannel` 函数结束前,向 `done` channel 发送一个空结构体作为完成信号。
`for i := 0; i < numWorkers; i++ {
使用 channel 的优缺点:

优点: 更灵活,可以传递更复杂的信息,是 Go 中 Goroutine 通信的基石。
缺点: 对于仅仅是“等待完成”这个场景,`sync.WaitGroup` 通常更简洁明了,也更符合意图。使用 channel 需要注意 channel 的缓冲和发送/接收的匹配,否则容易引入死锁。

何时选择哪种方式?

`sync.WaitGroup`: 当你的目标仅仅是等待一组 Goroutine 完成执行,而不需要在 Goroutine 之间传递数据时,这是最直接、最简洁的选择。它明确表达了“等待所有任务完成”的意图。
Channel: 当你需要 在 Goroutine 之间传递数据,或者在 Goroutine 完成时需要 通知主程序更多信息(而不仅仅是一个简单的信号)时,Channel 是更好的选择。例如,如果你需要从每个 Goroutine 获取计算结果,那么 Channel 就非常合适。

总结

要让程序在 `go` 关键字修饰的方法执行完成后再结束,本质上就是同步 Goroutine 的生命周期和主程序的生命周期。

最常用的方式是使用 `sync.WaitGroup`:`Add` 计数,`Done` 减计数,`Wait` 等待计数器归零。请务必在 Goroutine 中使用 `defer wg.Done()` 来保证即使发生异常也能正确通知 `WaitGroup`。
另一种方式是使用 channel:让 Goroutine 在完成时向 channel 发送信号,主程序则等待接收到足够数量的信号。

理解了这两种机制,你就能更好地控制并发程序中的流程,确保程序的健壮性和正确性。选择哪种方式取决于你的具体需求,但对于简单的等待完成场景,`sync.WaitGroup` 是首选。

网友意见

user avatar

并发控制的教程没看完吧..再回去修炼一下了解下sync包


Wg := &sync.WaitGroup{}

在你的go协程执行之前执行Wg.Add(1)

协程里的函数完成退出之前Wg.Done()

然后在主进程Wg.Wait()阻塞它

类似的话题

  • 回答
    在 Go 语言中,如果你想让程序在 `go` 关键字修饰的函数(通常称为 Goroutine)执行完成后再结束,你需要掌握 Goroutine 的同步和通信机制。这就像是给你的主程序一个信号,告诉它:“嘿,我这边还有一个正在忙活的家伙,等他忙完了,你再走。”下面我将详细讲解实现这一目标的几种常用方法.............
  • 回答
    好的,我们来详细深入地理解 Golang 中这句著名的口号:“不要通过共享内存来通信,而应该通过通信来共享内存”(Do not communicate by sharing memory; instead, share memory by communicating)。这句话是 Golang 设计哲.............
  • 回答
    Golang 1.5 是 Go 语言发展历程中的一个重要里程碑版本,于 2015 年 8 月发布。它带来了许多令人期待的改进和新特性,对 Go 的性能、工具链、语言特性以及生态系统都产生了深远的影响。下面我将从几个关键维度来详细评价 Golang 1.5 的更新: 1. 运行时与垃圾回收 (Runt.............
  • 回答
    Golang 的 goroutine 是一种非常轻量级的并发执行单元,它允许你以极低的成本同时运行大量的函数。与操作系统线程(OS Threads)相比,goroutine 的创建和切换开销要小得多,这使得 Golang 在并发编程方面具有显著优势。理解 goroutine 的实现,关键在于理解 G.............
  • 回答
    在 C 中实现 Go 语言 `select` 模式的精髓,即 等待多个异步操作中的任何一个完成,并对其进行处理,最贴切的类比就是使用 `Task` 的组合操作,尤其是 `Task.WhenAny`。Go 的 `select` 语句允许你监听多个通道(channel)的状态,当其中任何一个通道有数据可.............
  • 回答
    好的,咱们来聊聊告警业务里的“分发频率”这个事儿,而且还得考虑告警源随时会增增减减的情况。这事儿听起来有点复杂,但咱们一步步拆解,肯定能设计出一个稳健的方案。核心挑战:动态、个性化、高效咱们面对的核心问题是: 动态性 (Dynamic): 告警源不是固定不变的。新的告警源可能接入,旧的也可能下线.............
  • 回答
    2010 年前后诞生的编程语言,如 Go、Rust 和 Swift,它们普遍采用强类型和静态类型的组合,这并非偶然,而是反映了当时软件开发领域面临的挑战、技术进步以及对更高质量、更可靠软件的追求。下面我将详细解释为什么会出现这种趋势:核心概念:什么是强类型和静态类型?在深入探讨原因之前,我们先明确这.............
  • 回答
    如果要我放弃 Golang,那一定不是一时冲动,而是经过了深思熟虑,并且我得找到一个足够有力的替代方案,让我觉得“这值得”。毕竟,Golang 在很多方面做得还是相当不错的,尤其是它的并发模型和部署的便捷性,这几年确实帮我解决了不少问题。但话说回来,没有任何一种语言是完美的,也不是所有场景都适合 G.............
  • 回答
    Golang 团队在 2023 年 8 月份发布了一个新的字体项目,名为 Go Fonts。这个举动在软件开发领域并不常见,通常我们更关注语言本身的发展、库的更新或者工具链的改进。那么,Golang 为什么要发布一个新字体呢?这背后有着深思熟虑的原因和目标。要理解 Golang 发布新字体的动机,我.............
  • 回答
    没问题,咱们就来聊聊这些语言里的“协程”这玩意儿,它们听起来都挺炫酷,但骨子里还是有点小差别的。我尽量讲得深入点,把那些AI味儿的东西都去掉,让你一看就明白。 协程这玩意儿,为啥大家都爱?先别急着说区别,咱们先得明白为啥协程这么受欢迎。你想象一下,以前多线程编程那叫一个热闹,创建线程、切换上下文、同.............
  • 回答
    很多初次接触 Go 语言的开发者都会有一个疑问:“为什么 Go 语言没有三元运算符?” 这个问题其实触及到了 Go 设计哲学中的一些核心考量。要深入理解这一点,我们需要从多个角度去审视。什么是三元运算符?首先,我们得明确一下什么是三元运算符。它是一种特殊的运算符,顾名思义,它有三个操作数。最常见的形.............
  • 回答
    C++20 的协程(coroutines)和 Go 的 goroutines 都是用于实现并发和异步编程的强大工具,但它们的设计理念、工作方式以及适用的场景有显著的区别。简单地说,C++20 协程虽然强大且灵活,但与 Go 的 goroutines 在“易用性”和“轻量级”方面存在较大差距,不能完全.............
  • 回答
    这个问题很有意思,也触及到了Go语言设计哲学的一个核心点。确实,我们看到大多数现代编程语言,比如Java、C++、C、TypeScript、Swift等等,在声明和使用泛型时,不约而同地选择了尖括号 `<>`。而Go语言,在引入泛型时,却选择了中括号 `[]`。这并非“标新立异”那么简单,而是Go语.............
  • 回答
    Python 固然是一个全能选手,在数据科学、Web 开发、自动化脚本、机器学习等众多领域都表现出色,赢得了无数开发者的青睐。然而,你观察到的“很多企业转向 Golang”的现象,并非偶然,而是基于现实需求和技术演进的理性选择。这背后,并非 Python 不行,而是 Golang 在某些关键场景下,.............

本站所有内容均为互联网搜索引擎提供的公开搜索信息,本站不存储任何数据与内容,任何内容与数据均与本站无关,如有需要请联系相关搜索引擎包括但不限于百度google,bing,sogou

© 2025 tinynews.org All Rights Reserved. 百科问答小站 版权所有