如何在Go中同时运行多个函数

来自菜鸟教程
跳转至:导航、​搜索

作为 Write for DOnations 计划的一部分,作者选择了 Diversity in Tech Fund 来接受捐赠。

介绍

Go 语言的一个流行特性是它对 并发 的一流支持,或者说程序可以同时执行多项操作的能力。 随着计算机从更快地运行单个代码流转变为同时运行更多代码流,能够同时运行代码正在成为编程的重要组成部分。 为了更快地运行程序,程序员需要将他们的程序设计为并发运行,以便程序的每个并发部分可以独立于其他部分运行。 Go 中的两个特性 goroutineschannels 一起使用时使并发更容易。 Goroutine 解决了在程序中设置和运行并发代码的困难,通道解决了并发运行的代码之间安全通信的困难。

在本教程中,您将探索 goroutine 和通道。 首先,您将创建一个使用 goroutine 一次运行多个函数的程序。 然后,您将向该程序添加通道以在运行的 goroutine 之间进行通信。 最后,您将向程序中添加更多 goroutine,以模拟使用多个工作 goroutine 运行的程序。

先决条件

要遵循本教程,您将需要:

  • Go version 1.16 or greater installed. To set this up, follow the How To Install Go tutorial for your operating system.
  • 熟悉 Go 函数,可以在 如何在 Go 中定义和调用函数教程中找到。

使用 Goroutines 同时运行函数

在现代计算机中,处理器或 CPU 旨在同时运行尽可能多的代码流。 这些处理器具有一个或多个“核心”,每个“核心”都能够同时运行一个代码流。 因此,一个程序可以同时使用的内核越多,程序运行的速度就越快。 然而,为了让程序利用 多核 提供的速度提升,程序需要能够拆分成多个代码流。 将程序分成几部分可能是编程中更具挑战性的事情之一,但 Go 旨在让这变得更容易。

Go 执行此操作的一种方法是使用称为 goroutines 的功能。 goroutine 是一种特殊类型的函数,可以在其他 goroutine 也在运行时运行。 当一个程序被设计为一次运行多个代码流时,该程序被设计为同时运行[1]。 通常,当一个函数被调用时,它会在它继续运行之后完全在代码之前完成运行。 这被称为在“前台”运行,因为它会阻止您的程序在完成之前执行任何其他操作。 使用 goroutine,当 goroutine 在“后台”运行时,函数调用将立即继续运行下一个代码。 当代码在完成之前不阻止其他代码运行时,它被视为在后台运行。

goroutine 提供的强大功能是每个 goroutine 可以同时在一个处理器内核上运行。 如果你的计算机有四个处理器核心,而你的程序有四个 goroutine,那么所有四个 goroutine 可以同时运行。 当多个代码流像这样在不同的内核上同时运行时,称为并行运行。

为了可视化并发性和并行性之间的区别,请考虑下图。 当处理器运行一个函数时,它并不总是从头到尾同时运行它。 有时,当一个函数正在等待其他事情发生时,例如读取文件,操作系统会在 CPU 内核上交错其他函数、goroutine 或其他程序。 该图显示了为并发设计的程序如何在单核和多核上运行。 它还显示了当并行运行时,与在单核上运行时相比,goroutine 的更多段如何适应相同的时间范围(如图所示 9 个垂直段)。

图中左侧标为“并发”的列显示了围绕并发设计的程序如何通过运行 goroutine1 的一部分,然后是另一个函数、goroutine 或程序,然后是 goroutine2,然后再次 goroutine1,以此类推。 对于用户来说,这似乎是程序同时运行所有函数或 goroutine,即使它们实际上是一个接一个地以小部分运行。

图右侧标有“Parallelism”的列显示了同一程序如何在具有两个 CPU 内核的处理器上并行运行。 第一个 CPU 内核显示 goroutine1 运行与其他函数、goroutine 或程序,而第二个 CPU 内核显示 goroutine2 在该内核上运行其他函数或 goroutine。 有时 goroutine1goroutine2 会同时运行,只是在不同的 CPU 内核上。

该图还显示了 Go 的另一个强大特性, 可扩展性。 当一个程序可以在任何东西上运行时,它是可扩展的,从具有几个处理器内核的小型计算机到具有数十个内核的大型服务器,并利用这些额外的资源。 该图显示,通过使用 goroutine,您的并发程序能够在单个 CPU 内核上运行,但随着 CPU 内核的增加,可以并行运行更多的 goroutine 以加快程序速度。

要开始使用新的并发程序,请在您选择的位置创建一个 multifunc 目录。 您可能已经为您的项目创建了一个目录,但在本教程中,您将创建一个名为 projects 的目录。 您可以通过 IDE 或命令行创建 projects 目录。

如果您使用命令行,首先创建 projects 目录并导航到它:

mkdir projects
cd projects

projects 目录中,使用 mkdir 命令创建程序的目录 (multifunc),然后导航到它:

mkdir multifunc
cd multifunc

进入 multifunc 目录后,使用 nano 或您喜欢的编辑器打开一个名为 main.go 的文件:

nano main.go

main.go 文件中粘贴或键入以下代码以开始使用。

项目/多功能/main.go

package main

import (
    "fmt"
)

func generateNumbers(total int) {
    for idx := 1; idx <= total; idx++ {
        fmt.Printf("Generating number %d\n", idx)
    }
}

func printNumbers() {
    for idx := 1; idx <= 3; idx++ {
        fmt.Printf("Printing number %d\n", idx)
    }
}

func main() {
    printNumbers()
    generateNumbers(3)
}

这个初始程序定义了两个函数,generateNumbersprintNumbers,然后在 main 函数中运行这些函数。 generateNumbers 函数将要“生成”的数字数量作为参数,在本例中为 1 到 3,然后将这些数字中的每一个打印到屏幕上。 printNumbers 函数还没有接受任何参数,但它也会打印出数字一到三。

保存 main.go 文件后,使用 go run 运行它以查看输出:

go run main.go

输出将类似于以下内容:

OutputPrinting number 1
Printing number 2
Printing number 3
Generating number 1
Generating number 2
Generating number 3

您会看到函数一个接一个地运行,首先运行的是 printNumbers,其次是 generateNumbers

现在,假设 printNumbersgenerateNumbers 每个都需要三秒钟才能运行。 当同步运行 ' 或像上一个示例一样一个接一个地运行时,您的程序将需要 6 秒才能运行。 首先,printNumbers 会运行 3 秒,然后 generateNumbers 会运行 3 秒。 然而,在你的程序中,这两个函数是相互独立的,因为它们不依赖于另一个函数的数据来运行。 您可以利用这一点通过使用 goroutine 同时运行函数来加速这个假设的程序。 当两个函数同时运行时,理论上程序可以运行一半的时间。 如果 printNumbersgenerateNumbers 函数都需要三秒钟的时间来运行,并且两者都在完全相同的时间启动,则程序可以在三秒钟内完成。 (不过,实际速度可能会因外部因素而有所不同,例如计算机有多少内核或计算机上同时运行了多少其他程序。)

作为 goroutine 并发运行一个函数类似于同步运行一个函数。 要将函数作为 goroutine 运行(相对于标准同步函数),您只需在函数调用之前添加 go 关键字。

但是,要让程序同时运行 goroutine,您需要进行一项额外的更改。 您需要为您的程序添加一种方法,以等待两个 goroutine 完成运行。 如果你不等待你的 goroutines 完成并且你的 main 函数完成,goroutines 可能永远不会运行,或者它们只有一部分可以运行并且没有完成运行。

要等待函数完成,您将使用 Go 的 sync 包中的 WaitGroupsync 包包含“同步原语”,例如 WaitGroup,旨在同步程序的各个部分。 在您的情况下,同步会跟踪两个函数何时完成运行,以便您退出程序。

WaitGroup 原语通过使用 AddDoneWait 函数计算需要等待的次数来工作。 Add 函数将计数增加提供给函数的数字,Done 将计数减少一。 然后可以使用 Wait 函数等待计数达到零,这意味着 Done 已被调用足够多次以抵消对 Add 的调用。 一旦计数达到零,Wait 函数将返回,程序将继续运行。

接下来,更新 main.go 文件中的代码以使用 go 关键字将两个函数作为 goroutine 运行,并在程序中添加 sync.WaitGroup

项目/多功能/main.go

package main

import (
    "fmt"
    "sync"
)

func generateNumbers(total int, wg *sync.WaitGroup) {
    defer wg.Done()

    for idx := 1; idx <= total; idx++ {
        fmt.Printf("Generating number %d\n", idx)
    }
}

func printNumbers(wg *sync.WaitGroup) {
    defer wg.Done()

    for idx := 1; idx <= 3; idx++ {
        fmt.Printf("Printing number %d\n", idx)
    }
}

func main() {
    var wg sync.WaitGroup

    wg.Add(2)
    go printNumbers(&wg)
    go generateNumbers(3, &wg)

    fmt.Println("Waiting for goroutines to finish...")
    wg.Wait()
    fmt.Println("Done!")
}

在声明了 WaitGroup 之后,它需要知道要等待多少东西。 在启动 goroutine 之前在 main 函数中包含 wg.Add(2) 将告诉 wg 在考虑组完成之前等待两个 Done 调用。 如果在 goroutines 启动之前没有这样做,那么事情可能会发生混乱,或者代码可能会因为 wg 不知道它应该等待任何 Done 调用而发生混乱.

然后每个函数将在函数运行完成后使用 defer 调用 Done 将计数减一。 main 函数也更新为包括对 WaitGroup 上的 Wait 的调用,因此 main 函数将等到两个函数调用 [ X154X] 继续运行并退出程序。

保存 main.go 文件后,像以前一样使用 go run 运行它:

go run main.go

输出将类似于以下内容:

OutputPrinting number 1
Waiting for goroutines to finish...
Generating number 1
Generating number 2
Generating number 3
Printing number 2
Printing number 3
Done!

您的输出可能与此处打印的不同,甚至可能在您每次运行程序时发生变化。 由于这两个函数同时运行,输出取决于 Go 和您的操作系统为每个函数运行提供的时间。 有时有足够的时间完全运行每个函数,您会看到两个函数都不间断地打印它们的整个序列。 其他时候,您会看到像上面的输出一样散布的文本。

您可以尝试的一个实验是删除 main 函数中的 wg.Wait() 调用,并再次使用 go run 运行程序几次。 根据您的计算机,您可能会看到 generateNumbersprintNumbers 函数的一些输出,但也可能根本看不到它们的任何输出。 当您删除对 Wait 的调用时,程序将不再等待这两个函数完成运行后再继续。 由于 main 函数在 Wait 函数之后很快结束,因此您的程序很有可能会在 goroutine 完成运行之前到达 main 函数的末尾并退出。 发生这种情况时,您会看到打印出来的几个数字,但您不会看到每个函数的全部三个数字。

在本节中,您创建了一个程序,该程序使用 go 关键字同时运行两个 goroutine 并打印一系列数字。 您还使用了 sync.WaitGroup 让您的程序在退出程序之前等待这些 goroutine 完成。

您可能已经注意到 generateNumbersprintNumbers 函数没有返回值。 在 Go 中,goroutine 不能像标准函数那样返回值。 您仍然可以使用 go 关键字来调用返回值的函数,但是这些返回值将被丢弃,您将无法访问它们。 那么,当你在另一个 goroutine 中需要来自一个 goroutine 的数据时,如果你不能返回值,你会怎么做? 解决方案是使用称为“通道”的 Go 功能,它允许您将数据从一个 goroutine 发送到另一个 goroutine。

使用通道在 Goroutine 之间安全通信

并发编程中比较困难的部分之一是在同时运行的程序的不同部分之间进行安全通信。 如果您不小心,您可能会遇到只有并发程序才有可能出现的问题。 例如,当程序的两个部分同时运行时,可能会发生 数据争用 ,其中一部分尝试更新变量,而另一部分同时尝试读取它。 发生这种情况时,读取或写入可能会发生乱序,导致程序的一个或两个部分使用错误的值。 “数据竞赛”这个名称来自程序的两个部分相互“竞赛”以访问数据。

虽然在 Go 中仍然可能遇到数据竞争等并发问题,但该语言旨在更容易避免它们。 除了 goroutine 之外,通道是另一个使并发更安全、更易于使用的特性。 通道可以被认为是两个或多个不同的 goroutine 之间的管道,可以通过它发送数据。 一个 goroutine 将数据放入管道的一端,另一个 goroutine 将相同的数据取出。 为您处理确保数据安全地从一个到另一个的困难部分。

在 Go 中创建通道类似于使用内置 make() 函数创建 slice 的方式。 通道的类型声明使用 chan 关键字,后跟要在通道上发送的 数据类型 。 例如,要创建用于发送 int 值的通道,您将使用类型 chan int。 如果您想要一个用于发送 []byte 值的通道,则应为 chan []byte,如下所示:

bytesChan := make(chan []byte)

创建通道后,您可以使用带箭头的 <- 运算符在通道上发送或接收数据。 <- 运算符相对于通道变量的位置决定了您是从通道读取还是写入通道。

要写入通道,请从通道变量开始,然后是 <- 运算符,然后是要写入通道的值:

intChan := make(chan int)
intChan <- 10

要从通道读取值,请从要输入值的变量开始,=:= 为变量赋值,然后是 [X173X ] 运算符,然后是您要从中读取的频道:

intChan := make(chan int)
intVar := <- intChan

为了使这两个操作保持直截了当,记住 <- 箭头始终指向左侧(与 -> 相对),并且箭头指向值的去向会很有帮助。 在写入通道的情况下,箭头将值指向通道。 从通道读取时,箭头将通道指向变量。

像切片一样,也可以在 for 循环 中使用 range 关键字来读取通道。 当使用 range 关键字读取通道时,循环的每次迭代都会从通道中读取下一个值并将其放入循环变量中。 然后它将继续从通道读取,直到通道关闭或以其他方式退出 for 循环,例如 break

intChan := make(chan int)
for num := range intChan {
    // Use the value of num received from the channel
    if num < 1 {
        break
    }
}

在某些情况下,您可能只想允许函数读取或写入通道,但不能同时允许两者。 为此,您需要将 <- 运算符添加到 chan 类型声明中。 与从通道读取和写入类似,通道类型使用 <- 箭头允许变量将通道限制为仅读取、仅写入或同时读取和写入。 例如,要定义 int 值的只读通道,类型声明将是 <-chan int

func readChannel(ch <-chan int) {
    // ch is read-only
}

如果您希望通道是只写的,您可以将其声明为 chan<- int

func writeChannel(ch chan<- int) {
    // ch is write-only
}

请注意,箭头指向读取通道外,指向写入通道。 如果声明没有箭头,例如 chan int,则通道可用于读取和写入。

最后,一旦不再使用通道,可以使用内置的 close() 函数将其关闭。 这一步是必不可少的,因为当通道被创建然后在程序中多次闲置时,它可能导致所谓的 内存泄漏 。 内存泄漏是指程序创建的东西会耗尽计算机上的内存,但一旦完成使用它就不会将该内存释放回计算机。 随着时间的推移,这会导致程序慢慢地(或者有时不是那么慢)消耗更多的内存,就像漏水一样。 当使用 make() 创建通道时,计算机的一些内存被用于通道,然后当在通道上调用 close() 时,该内存将返回给计算机以供使用为了别的东西。

现在,更新程序中的 main.go 文件以使用 chan int 通道在您的 goroutine 之间进行通信。 generateNumbers 函数将生成数字并将它们写入通道,而 printNumbers 函数将从通道中读取这些数字并将它们打印到屏幕上。 在 main 函数中,您将创建一个新通道作为参数传递给其他每个函数,然后在通道上使用 close() 将其关闭,因为它将不再使用. generateNumbers 函数也不应该再是一个 goroutine,因为一旦该函数完成运行,程序将完成生成它需要的所有数字。 这样,close() 函数仅在两个函数完成运行之前在通道上调用。

项目/多功能/main.go

package main

import (
    "fmt"
    "sync"
)

func generateNumbers(total int, ch chan<- int, wg *sync.WaitGroup) {
    defer wg.Done()

    for idx := 1; idx <= total; idx++ {
        fmt.Printf("sending %d to channel\n", idx)
        ch <- idx
    }
}

func printNumbers(ch <-chan int, wg *sync.WaitGroup) {
    defer wg.Done()

    for num := range ch {
        fmt.Printf("read %d from channel\n", num)
    }
}

func main() {
    var wg sync.WaitGroup
    numberChan := make(chan int)

    wg.Add(2)
    go printNumbers(numberChan, &wg)

    generateNumbers(3, numberChan, &wg)

    close(numberChan)

    fmt.Println("Waiting for goroutines to finish...")
    wg.Wait()
    fmt.Println("Done!")
}

generateNumbersprintNumbers 的参数中,您会看到 chan 类型使用只读和只写类型。 由于 generateNumbers 只需要能够向通道写入数字,因此它是一种只写类型,其中 <- 箭头指向通道。 printNumbers 只需要能够从通道中读取数字,所以它是只读类型,<- 箭头指向远离通道。

尽管这些类型可能是 chan int,它允许读取和写入,但将它们限制为函数需要的内容可能会有所帮助,以避免意外导致您的程序从称为 [ X243X]死锁。 当程序的一部分正在等待程序的另一部分执行某项操作时,可能会发生死锁,但程序的另一部分也在等待程序的第一部分完成。 由于程序的两个部分都在相互等待,程序将永远不会继续运行,就像两个齿轮卡住一样。

由于 Go 中通道通信的工作方式,可能会发生死锁。 当程序的一部分正在写入通道时,它会等到程序的另一部分从该通道读取,然后再继续。 类似地,如果一个程序正在从一个通道读取,它会等到程序的另一部分写入该通道,然后再继续。 等待其他事情发生的程序的一部分被称为 blocking 因为它被阻止继续,直到发生其他事情。 通道在被写入或读取时会阻塞。 因此,如果您有一个函数,您希望写入通道但意外地从通道读取,您的程序可能会进入死锁,因为通道永远不会被写入。 确保这种情况永远不会发生是使用 chan<- int<-chan int 而不仅仅是 chan int 的原因之一。

更新代码的另一个重要方面是使用 close() 来关闭通道,一旦通道被 generateNumbers 写入。 在这个程序中,close() 导致 printNumbers 中的 for ... range 循环退出。 由于使用 range 从通道读取一直持续到它正在读取的通道关闭,如果 close 没有在 numberChan 上调用,那么 printNumbers 将永远不会结束。 如果 printNumbers 永远不会完成,那么当 printNumbers 退出时,defer 将永远不会调用 WaitGroupDone 方法。 如果 Done 方法从未从 printNumbers 调用,程序本身将永远不会退出,因为 WaitGroupWait 方法在 [X152X ] 函数将永远不会继续。 这是另一个死锁示例,因为 main 函数正在等待永远不会发生的事情。

现在,再次在 main.go 上使用 go run 命令运行更新后的代码。

go run main.go

您的输出可能与下面显示的略有不同,但总体上应该是相似的:

Outputsending 1 to channel
sending 2 to channel
read 1 from channel
read 2 from channel
sending 3 to channel
Waiting for functions to finish...
read 3 from channel
Done!

程序的输出显示 generateNumbers 函数正在生成数字 1 到 3,同时将它们写入与 printNumbers 共享的通道。 一旦 printNumbers 收到号码,它就会将其打印到屏幕上。 在 generateNumbers 生成所有三个数字后,它将退出,允许 main 函数关闭通道并等待 printNumbers 完成。 一旦 printNumbers 打印完最后一个数字,它就会在 WaitGroup 上调用 Done 并退出程序。 与之前的输出类似,您看到的确切输出将取决于各种外部因素,例如操作系统或 Go 运行时选择运行特定 goroutine 的时间,但应该相对接近。

使用 goroutine 和通道设计程序的好处是,一旦您设计了要拆分的程序,就可以将其扩展到更多的 goroutine。 由于 generateNumbers 只是写入通道,因此从该通道读取多少其他内容并不重要。 它只会将数字发送到任何读取通道的东西。 您可以通过运行多个 printNumbers goroutine 来利用这一点,因此它们中的每一个都将从同一个通道读取并同时处理数据。

现在您的程序正在使用通道进行通信,再次打开 main.go 文件并更新您的程序,使其启动多个 printNumbers goroutine。 您需要调整对 wg.Add 的调用,以便为您启动的每个 goroutine 添加一个。 您不必再担心在 WaitGroup 中添加一个以调用 generateNumbers 了,因为程序在没有完成整个功能的情况下将无法继续运行,这与您运行它时不同一个协程。 为确保它在完成时不会减少 WaitGroup 计数,您应该从函数中删除 defer wg.Done() 行。 接下来,将 goroutine 的编号添加到 printNumbers 可以更容易地查看每个通道是如何读取的。 增加生成的数字数量也是一个好主意,以便更容易看到分散的数字:

项目/多功能/main.go

...

func generateNumbers(total int, ch chan<- int, wg *sync.WaitGroup) {
    for idx := 1; idx <= total; idx++ {
        fmt.Printf("sending %d to channel\n", idx)
        ch <- idx
    }
}

func printNumbers(idx int, ch <-chan int, wg *sync.WaitGroup) {
    defer wg.Done()

    for num := range ch {
        fmt.Printf("%d: read %d from channel\n", idx, num)
    }
}

func main() {
    var wg sync.WaitGroup
    numberChan := make(chan int)

    for idx := 1; idx <= 3; idx++ {
        wg.Add(1)
        go printNumbers(idx, numberChan, &wg)
    }

    generateNumbers(5, numberChan, &wg)

    close(numberChan)

    fmt.Println("Waiting for goroutines to finish...")
    wg.Wait()
    fmt.Println("Done!")
}

更新 main.go 后,您可以使用 go runmain.go 再次运行程序。 在继续生成数字之前,您的程序应该启动三个 printNumbers 协程。 你的程序现在还应该生成五个数字而不是三个数字,以便更容易地看到数字分布在三个 printNumbers goroutine 中的每一个中:

go run main.go

输出可能与此类似(尽管您的输出可能会有很大差异):

Outputsending 1 to channel
sending 2 to channel
sending 3 to channel
3: read 2 from channel
1: read 1 from channel
sending 4 to channel
sending 5 to channel
3: read 4 from channel
1: read 5 from channel
Waiting for goroutines to finish...
2: read 3 from channel
Done!

当您这次查看程序输出时,很有可能它与您在上面看到的输出有很大不同。 由于有三个 printNumbers goroutines 正在运行,因此有一个机会因素来确定哪一个接收特定数字。 当一个 printNumbers goroutine 接收到一个数字时,它会花费少量时间将该数字打印到屏幕上,而另一个 goroutine 从通道中读取下一个数字并执行相同的操作。 当 goroutine 完成打印数字的工作并准备读取另一个数字时,它将返回并再次读取通道以打印下一个。 如果没有更多数字要从通道读取,它将开始阻塞,直到可以读取下一个数字。 一旦 generateNumbers 完成并在通道上调用 close(),所有三个 printNumbers goroutine 将完成它们的 range 循环并退出。 当所有三个 goroutine 都退出并在 WaitGroup 上调用 Done 时,WaitGroup 的计数将达到零,程序将退出。 您还可以尝试增加或减少生成的 goroutine 或数字的数量,以了解这对输出有何影响。

使用 goroutine 时,避免启动太多。 理论上,一个程序可能有数百甚至数千个 goroutine。 但是,根据运行程序的计算机,拥有更多数量的 goroutine 实际上可能会更慢。 使用大量 goroutine,它有可能会遇到 资源匮乏 。 每次 Go 运行一个 goroutine 的一部分时,除了运行下一个函数中的代码所需的时间之外,它还需要一点额外的时间才能再次开始运行。 由于需要额外的时间,计算机在运行每个 goroutine 之间切换可能比实际运行 goroutine 本身花费更长的时间。 当这种情况发生时,它被称为资源匮乏,因为程序及其 goroutine 没有获得它们运行所需的资源,或者获得的资源很少。 在这些情况下,减少程序中同时运行的部分数量可能会更快,因为这将减少在它们之间切换所需的时间,并为运行程序本身提供更多时间。 记住程序运行在多少个内核上可以是决定要使用多少个 goroutine 的一个很好的起点。

使用 goroutine 和通道的组合可以创建非常强大的程序,这些程序能够从小型台式计算机上扩展到大型服务器。 正如您在本节中看到的,通道可用于在少至几个 goroutine 到潜在的数千个 goroutine 之间进行通信,而只需进行最小的更改。 如果您在编写程序时考虑到这一点,您将能够利用 Go 中可用的并发性为您的用户提供更好的整体体验。

结论

在本教程中,您使用 go 关键字创建了一个程序来启动并发运行的 goroutine,这些 goroutine 在运行时打印出数字。 该程序运行后,您使用 make(chan int) 创建了一个新的 int 值通道,然后使用该通道在一个 goroutine 中生成数字并将它们发送到另一个 goroutine 以打印到屏幕上。 最后,您同时启动了多个“打印”goroutines,作为一个示例,说明如何使用通道和 goroutines 在多核计算机上加速您的程序。

如果您有兴趣了解有关 Go 并发的更多信息,Go 团队创建的 Effective Go 文档会更详细地介绍。 并发不是并行 Go 博客文章也是关于并发和并行之间关系的有趣后续,这两个术语有时被错误地混为一谈。

本教程也是 DigitalOcean How to Code in Go 系列的一部分。 该系列涵盖了许多 Go 主题,从第一次安装 Go 到如何使用语言本身。