ZetCode

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,开发者可以在不涉及传统线程模型复杂性的情况下,通过高效地管理并发任务来优化性能。

按顺序执行函数

下面的示例按顺序运行一个函数。

main.go
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 函数。

main.go
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 完成。

main.go
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 进行异步请求。

async_req.go
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 之间发送和接收值。

main.go
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 计算斐波那契数。

main.go
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 的用法。

作者

我叫 Jan Bodnar,我是一名充满热情的程序员,拥有丰富的编程经验。我自 2007 年以来一直在撰写编程文章。迄今为止,我已撰写了 1400 多篇文章和 8 本电子书。我在编程教学方面拥有十多年的经验。

列出所有 Go 教程