透過三個例子,學習 Go 語言併發程式設計的利器 - goroutine

發表於2024-02-11

Go 語言(也稱為 Golang)是一門由 Google 開發的程式語言,以其簡潔、高效和對併發程式設計的內建支援而在程式設計領域享有盛名。

在 Go 語言中,goroutine 是一項強大的併發特性,用於輕量級執行緒的建立和管理。

本文將向沒有接觸過 Go 語言的朋友,介紹 goroutine 的概念、使用場合,並提供具體的例子以演示其用法。

1. Goroutine 的概念:

Goroutine 是 Go 語言中用於併發執行的輕量級執行緒。與傳統的執行緒相比,goroutine 的建立和銷燬成本很低,可以在同一個程式中輕鬆建立多個 goroutine 而不會導致效能下降。Goroutines 之間透過通道(channel)進行通訊,實現了併發程式設計的簡潔和安全。

2. Goroutine的使用場合:

俗話說,紅粉贈佳人,寶劍送烈士。既然 Goroutine 有如此突出的能力,併發程式設計領域就是它大顯身手的舞臺。

在下列這些應用場景裡,我們都能看到 Goroutine 施展拳腳的身影。

  • 併發執行任務: Goroutine 可用於同時執行多個任務,提高程式的效能。
  • 非阻塞 I/O 操作: 在進行 I/O 操作時,可以使用 goroutine 確保其他任務繼續執行,而不是同步等待 I/O 完成。
  • 事件驅動程式設計: Goroutine 可用於處理事件,如監聽 HTTP 請求、處理使用者輸入等。
  • 併發演算法: 實現一些需要平行計算的演算法,透過 goroutine 可以更輕鬆地管理併發執行的部分。
  • 定時任務: 使用 goroutine 和定時器可以實現定時執行的任務。

3. Goroutine 的具體例子:

我們透過一些具體的例子來說明。

例子1:簡單的併發任務執行

作為熱身,我們透過一些簡單的 Hello World 級別的例子來熟悉 Goroutine 的用法。

原始碼如下:

package main

import (
    "fmt"
    "sync"
)

func printNumbers(wg *sync.WaitGroup) {
    defer wg.Done()
    for i := 1; i <= 5; i++ {
        fmt.Printf("%d ", i)
    }
}

func printLetters(wg *sync.WaitGroup) {
    defer wg.Done()
    for char := 'a'; char <= 'e'; char++ {
        fmt.Printf("%c ", char)
    }
}

func main() {
    var wg sync.WaitGroup

    wg.Add(2)
    go printNumbers(&wg)
    go printLetters(&wg)

    wg.Wait()
}

將上述原始碼另存為一個 .go 檔案裡,比如取名 1.go, 然後執行命令列 go run 1.go, 看到下面的輸出:

在這個例子中,我們透過關鍵字 go,建立了兩個 goroutine,一個用於列印數字,另一個用於列印字母。sync.WaitGroup 函式呼叫,用於等待這兩個 goroutine 執行完畢。這是 go 語言併發程式設計裡,最常用的同步機制之一。

例子2:併發下載多個網頁

原始碼如下:

package main

import (
    "fmt"
    "io/ioutil"
    "net/http"
    "sync"
)

func fetch(url string, wg *sync.WaitGroup) {
    defer wg.Done()

    response, err := http.Get(url)
    if err != nil {
        fmt.Printf("Error fetching %s: %v\n", url, err)
        return
    }
    defer response.Body.Close()

    body, err := ioutil.ReadAll(response.Body)
    if err != nil {
        fmt.Printf("Error reading response body from %s: %v\n", url, err)
        return
    }

    fmt.Printf("Length of %s: %d\n", url, len(body))
}

func main() {
    var wg sync.WaitGroup

    urls := []string{"https://www.baidu.com", "https://cloud.tencent.com/", "https://www.qq.com/"}

    for _, url := range urls {
        wg.Add(1)
        go fetch(url, &wg)
    }
    wg.Wait()
}

執行上面的原始碼後,在控制檯看到下列輸出,列印了三個網站(百度,qq 和騰訊雲社群)首頁 html 檔案的長度(length)。

這個例子展示瞭如何使用 goroutine 併發地下載多個網頁。每個網頁都在單獨的goroutine中進行下載,從而減少了完成下載任務所需花費的時間。

從原始碼可以看出,如果是用 Java 這門傳統的程式語言完成這個需求的編碼工作,我們需要使用 Java 提供的 Thread 類和一些繁瑣的同步機制程式碼。而使用 Go 語言,透過 go fetch(url, &wg) 這一行短小精悍的語言,就可以輕鬆完成 goroutine 的啟動和根據 url 讀取 HTML 頁面資料的操作,程式碼大大簡化。

例子3:透過通道實現生產者-消費者模型

生產者-消費者是作業系統課程裡必學的概念之一。我們使用 func 關鍵字,分別定義了生產者和消費者兩個函式。生產者函式,使用 time.Sleep 方法,每隔 0.5 秒鐘生產一個正整數序列 1,2,3,4,5. 消費者函式,相應的每隔 0.5 秒鐘消費一個正整數。

package main

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

func producer(ch chan<- int, wg *sync.WaitGroup) {
    defer wg.Done()
    for i := 1; i <= 5; i++ {
        ch <- i
fmt.Printf("Produced: %d\n", i)
        time.Sleep(time.Millisecond * 500) // 模擬生產過程的延遲
    }
    close(ch)
}

func consumer(ch <-chan int, wg *sync.WaitGroup) {
    defer wg.Done()
    for num := range ch {
        fmt.Printf("Consumed: %d\n", num)
        time.Sleep(time.Millisecond * 200) // 模擬消費過程的延遲
    }
}

func main() {
    var wg sync.WaitGroup

    ch := make(chan int, 3) // 建立帶緩衝通道,容量為3

    wg.Add(2)
    go producer(ch, &wg)
    go consumer(ch, &wg)

    wg.Wait()
}

執行上面的 go 原始碼,輸出如下,生產一個正整數序列,然後消費,以此類推。

這個例子演示瞭如何使用 goroutine 和通道實現生產者-消費者模型。生產者將資料傳送到通道,而消費者從通道中接收並處理資料,透過 goroutine 實現了併發的生產和消費過程。

我們使用了帶緩衝的通道,容量為3。帶緩衝的通道允許在通道未被完全讀取的情況下進行寫入,這在生產者和消費者速度不一致時非常有用。

在生產者函式 producer 裡,在ch 是一個只寫的整數通道,wg 是一個 sync.WaitGroup 型別的指標,用於等待所有的goroutine完成。在這個函式中,生產者透過 ch <- i 向通道傳送整數 i,表示生產了一個產品。然後,透過 fmt.Printf 列印出生產的產品的資訊,並透過 time.Sleep 模擬生產過程的延遲。最後,透過 close(ch) 關閉通道,表示生產者不再向通道傳送資料。

再看消費者函式 consumer. ch 是一個只讀的整數通道,wg 是一個 sync.WaitGroup 型別的指標。在這個函式中,消費者透過 num := <-ch 從通道接收整數,表示消費了一個產品。然後,透過 fmt.Printf 列印出消費的產品的資訊,並透過 time.Sleep 模擬消費過程的延遲。這個迴圈會一直執行,直到通道被關閉,此時 range ch 將會退出。

總結

透過本文介紹的這三個例子,我們可以看到 goroutine 的強大之處。透過goroutine 和 channel 的組合,以及 sync.WaitGroup 的使用,Go 語言提供了強大而簡潔的併發程式設計機制。相信之前有過 Java 程式設計經驗的開發者,對於 Java 和 Go 這兩門程式語言,在實現同一需求所需的不同程式碼量的差異,更是深有體會。