Go Goroutine
最后修改于 2025 年 5 月 3 日
在本篇文章中,我们将展示如何在 Golang 中使用 goroutines。Goroutine 是一种轻量级的执行线程。
定义
Goroutine 是 Go 中的一种轻量级执行线程,专为高效的并发处理而设计。Goroutines 允许函数独立于其他正在执行的代码运行,这对于构建响应式和可扩展的应用程序至关重要。
与传统的线程不同,Goroutines 由 Go 运行时管理,不需要显式的创建或同步机制,因此在内存和处理开销方面更有效。但是,并发执行并不总是保证并行执行;Goroutines 是否并行运行取决于可用的 CPU 资源和 Go 的调度器。
每个 Go 程序至少有一个 goroutine — main goroutine — 它在程序开始执行时启动。可以使用 go 关键字启动其他 goroutines,从而允许函数异步执行。
go myFunction()
在此示例中,myFunction 作为单独的 goroutine 启动,使其能够与程序的其他部分并发运行。如果没有适当的同步机制,例如 sync.WaitGroup 或 channels,如果主函数过早退出,goroutine 可能会在完成之前终止。
通过利用 goroutines,开发者可以在不涉及传统线程模型复杂性的情况下,通过高效地管理并发任务来优化性能。
按顺序执行函数
下面的示例按顺序运行一个函数。
package main
import (
"fmt"
)
func main() {
hello("Martin")
hello("Lucia")
hello("Michal")
hello("Jozef")
hello("Peter")
}
func hello(name string) {
fmt.Printf("Hello %s!\n", name)
}
程序按顺序运行 hello 函数。
$ go run main.go Hello Martin! Hello Lucia! Hello Michal! Hello Jozef! Hello Peter!
输出始终相同。
并发执行函数
现在我们并发运行 hello 函数。
package main
import (
"fmt"
)
func main() {
go hello("Martin")
go hello("Lucia")
go hello("Michal")
go hello("Jozef")
go hello("Peter")
fmt.Scanln()
}
func hello(name string) {
fmt.Printf("Hello %s!\n", name)
}
使用 go 关键字,我们并发运行 hello 函数。fmt.Scanln 函数等待用户的输入。如果我们注释掉这个函数,程序会在我们看到 goroutines 的输出之前结束。
$ go run main.go Hello Lucia! Hello Michal! Hello Martin! Hello Jozef! Hello Peter! $ go run main.go Hello Martin! Hello Peter! Hello Lucia! Hello Michal! Hello Jozef!
我们运行了两次程序。请注意输出是不同的。
Go sync.WaitGroup
sync.WaitGroup 是一种同步工具,它等待一组 goroutines 完成。
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var wg sync.WaitGroup
wg.Add(2)
go func() {
count("oranges")
wg.Done()
}()
go func() {
count("apples")
wg.Done()
}()
wg.Wait()
}
func count(thing string) {
for i := 0; i < 4; i++ {
fmt.Printf("counting %s\n", thing)
time.Sleep(time.Millisecond * 500)
}
}
在程序中,我们使用 sync.WaitGroup 同步两个 goroutines 的执行。
var wg sync.WaitGroup wg.Add(2)
使用 Add,我们告诉等待多少个 goroutines。
go func() {
count("oranges")
wg.Done()
}()
我们创建一个匿名 goroutine。我们用 Done 告诉 Go 运行时 goroutine 已完成。
wg.Wait()
Wait 函数会阻塞,直到所有 goroutines 都完成。
time.Sleep(time.Millisecond * 500)
在演示程序中,通常使用 time.Sleep 来减慢 goroutines 的执行速度。
$ go run main.go counting apples counting oranges counting apples counting oranges counting oranges counting apples counting apples counting oranges
Go 异步请求
下一个示例使用 goroutines 进行异步请求。
package main
import (
"fmt"
"io/ioutil"
"log"
"net/http"
"regexp"
"sync"
)
func main() {
urls := []string{
"http://webcode.me",
"https://example.com",
"http://httpbin.org",
"https://perl.net.cn",
"https://php.ac.cn",
"https://pythonlang.cn",
"https://vscode.js.cn",
"https://clojure.org",
}
var wg sync.WaitGroup
for _, u := range urls {
wg.Add(1)
go func(url string) {
defer wg.Done()
content := doReq(url)
title := getTitle(content)
fmt.Println(title)
}(u)
}
wg.Wait()
}
func doReq(url string) (content string) {
resp, err := http.Get(url)
if err != nil {
log.Println(err)
return
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Println(err)
return
}
return string(body)
}
func getTitle(content string) (title string) {
re := regexp.MustCompile("<title>(.*)</title>")
parts := re.FindStringSubmatch(content)
if len(parts) > 0 {
return parts[1]
} else {
return "no title"
}
}
我们发起多个异步 HTTP 请求。我们获取每个网页的 title 标签的内容。每个请求都包装在一个 goroutine 中。
go func(url string) {
defer wg.Done()
content := doReq(url)
title := getTitle(content)
fmt.Println(title)
}(u)
在 goroutine 中,我们生成一个 GET 请求,接收响应,从响应中获取标题并将其打印到终端。
$ go run main.go The Perl Programming Language - www.perl.org Welcome to Python.org Visual Studio Code - Code Editing. Redefined PHP: Hypertext Preprocessor Example Domain httpbin.org Clojure My html page
Goroutine channels
Goroutines 通过 channels 进行通信。它们允许使用 channel 运算符 <- 发送和接收值。
c := make(chan string)
使用 make 函数创建一个新的 channel。
c <- v // send v := <-c // receive
Channel 运算符在 goroutines 之间发送和接收值。
package main
import (
"fmt"
"time"
)
func main() {
c := make(chan string)
go hello("Martin", c)
for msg := range c {
fmt.Println(msg)
}
}
func hello(name string, c chan string) {
for i := 0; i < 5; i++ {
msg := fmt.Sprintf("Hello %s!", name)
c <- msg
time.Sleep(time.Millisecond * 500)
}
close(c)
}
在该程序中,两个 goroutines 进行通信:main 和 hello。
c := make(chan string)
使用 make 创建一个 channel。
go hello("Martin", c)
使用 go 创建一个 hello goroutine。我们将 channel 作为参数传递。
for msg := range c {
fmt.Println(msg)
}
使用 range 关键字,我们遍历消息并将它们打印到控制台。
func hello(name string, c chan string) {
for i := 0; i < 5; i++ {
msg := fmt.Sprintf("Hello %s!", name)
c <- msg
time.Sleep(time.Millisecond * 500)
}
close(c)
}
在 hello 函数中,我们创建了五个消息并通过 channel 发送给 main goroutine。当 goroutine 完成后,我们使用 close 关闭 channel。
$ go run main.go Hello Martin! Hello Martin! Hello Martin! Hello Martin! Hello Martin!
使用 goroutines 计算斐波那契值
在下一个示例中,我们将使用 goroutines 计算斐波那契数。
package main
import (
"fmt"
)
func fib(n int, c chan int) {
x, y := 0, 1
for i := 0; i < n; i++ {
c <- x
x, y = y, x+y
}
close(c)
}
func main() {
c := make(chan int, 10)
go fib(cap(c), c)
for i := range c {
fmt.Println(i)
}
}
一系列斐波那契值在 fib goroutine 中生成。这些值通过 channel 一个一个地发送给调用方 goroutine。
$ go run main.go 0 1 1 2 3 5 8 13 21 34
来源
The Go Programming Language Specification
这是 Golang goroutines 的入门教程。我们提供了一些简单的例子来演示 goroutines 的用法。