此篇是用來紀錄我理解的 go routine 與相關範例代碼,藉由 ChatGPT 輔助理解和相關解釋,並配合實際嘗試與筆記來理解功能與特性。

Go routine

ChatGPT 解釋:

Go 語言的 goroutine 是一個非常輕量級的執行緒,它可以在一個程序中同時執行多個任務。和傳統的執行緒相比,goroutine 的開銷非常小,可以同時啟動數千甚至數萬個 goroutine,而不會造成系統資源的浪費和效能下降。Go 語言提供了 go 關鍵字來啟動一個新的 goroutine。當遇到 go 關鍵字時,Go 語言會在新的 goroutine 中執行該函數,而不會阻塞主執行緒的運行。這意味著我們可以同時執行多個任務而不需要等待前一個任務完成。

可以啟動多執行緒,也不會阻擋主程序的進程,以我理解的方式就是 Redis Queue 的運作,但一個是 service 一個則是內存功能,可能有些不太一樣的部分但如果是這樣理解會對我會比較好明白。就是執行到時不會阻塞主要流程。

相關範例代碼:

package main

import (
    "fmt"
    "time"
)

func count(name string) {
    for i := 1; i <= 5; i++ {
        fmt.Println(name, "count:", i)
        time.Sleep(time.Second)
    }
}

func main() {
    go count("goroutine") // 啟動 goroutine
    count("main") // 在主 goroutine 中執行 count
}

// output:
// main count: 1
// goroutine count: 1
// main count: 2
// goroutine count: 2
// main count: 3
// goroutine count: 3
// main count: 4
// goroutine count: 4
// main count: 5
// goroutine count: 5

這邊會看到是交叉的出現計數,為什麼需要放入 time.Sleep(time.Second) 呢?因為如果主程序結束的話 go routine 也會直接中斷運行,所以才會這樣子,另外一個範例:

package main

import (
    "fmt"
    "time"
)

func longOperation() {
    time.Sleep(5 * time.Second) // 模擬耗時操作
    fmt.Println("完成耗時操作")
}

func main() {
    fmt.Println("啟動goroutine")
    go longOperation() // 啟動goroutine
    fmt.Println("繼續執行主函式")
    time.Sleep(10 * time.Second) // 模擬等待耗時操作
    fmt.Println("END")
}

// 啟動goroutine
// 繼續執行主函式
// 完成耗時操作
// END

主程序整個等待時間為:time.Sleep(10 * time.Second),而 go routine 裡面則為 Sleep(5 * time.Second) 因此程序可以順利的完成,但如果今天把數值調整一下之後會發生:

time.Sleep(5 * time.Second) // 模擬耗時操作 這邊不調整
time.Sleep(2 * time.Second) // 模擬等待耗時操作 這邊改為<5

// 啟動goroutine
// 繼續執行主函式
// END

這邊可以看到 go routine 的 print 沒被執行到,或是執行到一半被中斷,原因是因為主程序已經執行完強迫結束應用,ChatGPT 資料如下:

在 Go 中,主程序(main)和 goroutine 之間的執行存在幾個注意事項:

  1. 主程序的執行不會等待 goroutine 完成,除非明確地使用同步原語(如通道)來等待 goroutine 的結果。
  2. 當主程序結束時,所有還在運行的 goroutine 也會被停止。
  3. 在 goroutine 中發生的 panic 不會影響其他 goroutine 的執行,也不會影響主程序的執行。
  4. goroutine 需要佔用一定的資源(如線程、內存等),如果過度使用 goroutine,可能會導致系統資源不足,進而導致程序崩潰。

因此,在使用 goroutine 時,需要注意以下幾點:

  1. 在啟動 goroutine 前,要確保所有的變量都被初始化或者設置好。
  2. 在啟動 goroutine 前,要確保所有的依賴都被解決或者明確地處理好。
  3. 在 goroutine 中,需要避免競爭條件和死鎖問題。可以使用通道、互斥鎖等同步原語來解決這些問題。
  4. 如果啟動了大量的 goroutine,需要注意控制其數量,避免過度佔用系統資源。

Channel

ChatGPT 解釋: 在 Go 中,channel 是一種用於在 goroutine 之間進行通信的原語。channel 可以看作是一個管道,goroutine 可以通過管道進行數據的傳輸和同步,以實現並發執行。

以下是 Go channel 的一些特性:

  1. 創建 channel:可以使用內建的 make 函數來創建 channel。例如,make(chan int) 創建了一個傳遞整數的 channel。
  2. 發送和接收數據:可以使用 <- 運算符來發送和接收數據。例如,ch <- 42 發送數字 42 到 channel,x := <-ch 從 channel 中接收一個數字。
  3. 非阻塞操作:如果 channel 中沒有數據或者已滿,發送操作和接收操作會被阻塞。可以使用 default 分支來實現非阻塞的發送和接收操作。
  4. 单向 channel:可以使用 chan<- 和 <-chan 來定義發送和接收操作的 channel。例如,ch1 := make(chan<- int) 定義了一個只能發送數字的 channel。
  5. 關閉 channel:可以使用 close 函数來關閉 channel。關閉後,接收操作會繼續從 channel 中讀取數據,直到 channel 中的所有數據都被讀取完畢。 總之,Go channel 是一種用於實現 goroutine 之間通信和同步的重要原語。通過使用 channel,可以實現並發執行,避免競爭條件和死鎖問題,提高程序的可靠性和穩定性。

這邊以一個不錯的範例作為理解,結合 go routine 機制,用於併發檢查網站服務狀態:

package main

import (
    "fmt"
    "net/http"
)

func crawl(url string, ch chan<- string) {
    resp, err := http.Get(url) // 發送http請求
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    defer resp.Body.Close()

    ch <- fmt.Sprintf("URL: %s, Status: %s", url, resp.Status) // 將數據傳遞到channel中
}

func main() {
    urls := []string{
        "https://www.google.com",
        "https://www.facebook.com",
        "https://www.github.com",
    }

    ch := make(chan string) // 創建一個string型的channel

    for _, url := range urls {
        go crawl(url, ch) // 同時啟動多個goroutine,從不同的網站上收集數據
    }

    for i := 0; i < len(urls); i++ {
        fmt.Println(<-ch) // 從channel中讀取數據並輸出
    }
}

// URL: https://www.google.com, Status: 200 OK
// URL: https://www.facebook.com, Status: 200 OK
// URL: https://www.github.com, Status: 200 OK

我這邊的理解是 Channel(in-memory) 對標的是我理解的 Redis(database) 機制,並且是 FIFO 機制(Queue)。也可以將 channel 當成一種 flag 等待操作,也可以透過多 channel 來實現相互等待的機制:

package main

import (
    "fmt"
    "time"
)

func doWork(flag chan bool) {
    for {
        select {
        case <-flag:
            fmt.Println("Flag is set. Stopping the work.")
            return
        default:
            fmt.Println("Doing some work...")
            time.Sleep(time.Second)
        }
    }
}

func main() {
    flag := make(chan bool)

    go doWork(flag)

    time.Sleep(5 * time.Second)
    fmt.Println("Setting flag to stop the work...")
    flag <- true

    time.Sleep(2 * time.Second)
    fmt.Println("Exiting...")
}

// Doing some work...
// Doing some work...
// Doing some work...
// Doing some work...
// Doing some work...
// Setting flag to stop the work...
// Flag is set. Stopping the work.
// Exiting...