Golang channel


簡介

channel 是 Go 語言中的一個核心類型,可以把它看成管道。並發核心單元通過它就可以發送或者接收數據進行通訊,這在一定程度上又進一步降低了編程的難度。

channel 是一個數據類型,主要用來解決 go 程的同步問題以及 go 程之間數據共享(數據傳遞)的問題。

goroutine 運行在相同的地址空間,因此訪問共享內存必須做好同步。goroutine 奉行通過通信來共享內存,而不是共享內存來通信。

引⽤類型 channel 可用於多個 goroutine 通訊。其內部實現了同步,確保並發安全(通過 CSP)。

channel

強調一下:

channel 是一個數據類型,對應一個“管道(通道)”。

定義 channel 變量

和 map 類似,channel 也是一個對應 make 創建的底層數據結構的引用

既然是引用, 那么我們在傳參的時候就能完成在 A 函數棧幀內修改 B 函數棧幀數據的目的. 說白了就是傳的地址.

當我們復制一個 channel 或用於函數參數傳遞時,我們只是拷貝了一個 channel 引用,因此調用者和被調用者將引用同一個 channel 對象。 和其它的引用類型一樣,channel 的零值也是 nil。

定義一個 channel 時,也需要定義發送到 channel 的值的類型。channel 可以使用內置的 make() 函數來創建:

make(chan Type)  // 等價於 make(chan Type, 0)
make(chan Type, capacity)
  • chan 是創建 channel 所需使用的關鍵字。
  • Type 代表指定 channel 收發數據的類型。

當參數 capacity = 0 時,channel 是無緩沖阻塞讀寫的;當 capacity > 0 時,channel 有緩沖、是非阻塞的,直到寫滿 capacity 個元素才阻塞寫入

channel 非常像生活中的管道,一邊可以存放東西,另一邊可以取出東西。channel 通過操作符 <- 來接收和發送數據,發送和接收數據語法:

channel <- value  // 發送 value 到 channel
<- channel  // 接收並將其丟棄
x := <- channel  // 從 channel 中接收數據, 並賦值給 x
x, ok := <- channel  // 功能同上, 同時檢查通道是否已關閉或者是否為空

默認情況下,channel 接收和發送數據都是阻塞的,除非另一端已經准備好,這樣就使得 goroutine 同步變的更加的簡單,而不需要顯式的 lock

我們先看一下沒有用 channel 的例子:

package main

import (
	"fmt"
	"time"
)

// 定義一個打印機
func printer(s string) {
	for _, value := range s {
		fmt.Printf("%c", value)
		time.Sleep(time.Millisecond * 300)
	}
}

/* 定義兩個人使用打印機 */
func person1() {
	printer("hello")
}

func person2() {
	printer("world")
}

func main() {
	go person1()
	go person2()
	time.Sleep(time.Second * 5) // 注意,只寫上面兩行會直接運行完畢,想一想 go 程的特性
}

結果:

hwoelrllod

那么,怎么用 channel 實現來保證順序輸出呢?

因為,person1 與 person2 都需要用一個 channel,所以要在全局定義一個 channel。具體代碼如下:

PS:你要傳的什么類型數據與 channel 中定義的類型沒有必然的聯系。

package main

import (
	"fmt"
	"time"
)

// 全局定義一個 channel,用來完成數據同步
var ch = make(chan int) // 傳的什么類型數據與 channel 中定義的類型沒有必然的聯系

// 定義一個打印機
func printer(s string) {
	for _, value := range s {
		fmt.Printf("%c", value)
		time.Sleep(time.Millisecond * 300)
	}
}

/* 定義兩個人使用打印機 */
func person1() {
	printer("hello")
	ch <- 777
}

func person2() {
	<-ch
	printer("world")
}

func main() {
	go person1()
	go person2()
	time.Sleep(time.Second * 3) // 注意,只寫上面兩行會直接運行完畢,想一想 go 程的特性
}

這個時候,當運行 person2 函數時,會阻塞在 <-ch 處,運行 person1 函數時,打印完 “hello”,會在 ch <- 777 處阻塞。

但是這時,ch <- 777 對應這寫端已經准備好了,同時 <-ch 對應讀端也已經准備好了,所以代碼就會繼續執行,接下來就會打印 “world”。

我們再來看一段代碼:

package main

import "fmt"

func main() {
	c := make(chan int)
	go func() {
		defer fmt.Println("子 go 程結束")
		fmt.Println("子 go 程正在運行 ...")
		c <- 666 /// 把 666 發送到 c
	}()

	num := <-c // 從 c 中接收數據, 並賦值給 num
	fmt.Println("num = ", num)
	fmt.Println("main go 程結束")
}

運行結果:

子 go 程正在運行 ...
子 go 程結束
num =  666
main go 程結束

以上我們都是用 channel 用來做數據同步,並沒有用到 channel 中的數據,下面我們看一個用 channel 完成數據傳遞的例子:

package main

import "fmt"

func main() {
	ch := make(chan string)
	// len(ch): channel 中剩余未讀取的數據個數; cap(ch): channel 的容量
	fmt.Println("len(ch) = ", len(ch), "cap(ch) = ", cap(ch))
	go func() {
		for i := 0; i < 2; i++ {
			fmt.Println("i = ", i)
		}
		ch <- "子 go 程打印完畢"
	}()
	str := <-ch
	fmt.Println(str)
}

注意:len(ch): channel 中剩余未讀取的數據個數; cap(ch): channel 的容量

運行結果:

len(ch) =  0 cap(ch) =  0
i =  0
i =  1
子 go 程打印完畢

強調一下:

channel 有兩個端:

  • 寫端(傳入端):chan <- 777
  • 讀端(傳出端):<- chan

要求:讀端和寫端必須同時滿足條件(讀端有數據可讀,寫端有數據可寫),才能在 channel 中完成數據流動。否則,阻塞。

【補充知識點】

每當有一個進程啟動時,系統會自動打開三個文件:標准輸入、標准輸出、標准錯誤,對應三個文件:stdin、stdout、stderr。

當進程運行結束時,系統會自動關閉這三個文件。

無緩沖的channel - 同步通信

無緩沖的通道(unbuffered channel)是指在接收前沒有能力保存任何值的通道。

這種類型的通道要求發送 goroutine 和接收 goroutine 同時准備好,才能完成發送和接收操作。否則,通道會導致先執行發送或接收操作的 goroutine 阻塞等待。

這種對通道進行發送和接收的交互行為本身就是同步的。其中任意一個操作都無法離開另一個操作單獨存在。

阻塞:由於某種原因數據沒有到達,當前協程(線程)持續處於等待狀態,直到條件滿足,才接觸阻塞。

同步:在兩個或多個協程(線程)間,保持數據內容一致性的機制。

下圖展示兩個 goroutine 如何利用無緩沖的通道來共享一個值:

使用無緩沖channel在goroutine之間同步

簡單說明:

  • 在第 1 步,兩個 goroutine 都到達通道,但哪個都沒有開始執行發送或者接收。
  • 在第 2 步,左側的 goroutine 將它的手伸進了通道,這模擬了向通道發送數據的行為。這時,這個 goroutine 會在通道中被鎖住,直到交換完成。
  • 在第 3 步,右側的 goroutine 將它的手放入通道,這模擬了從通道里接收數據。這個 goroutine 一樣也會在通道中被鎖住,直到交換完成。
  • 在第 4 步和第 5 步,進行交換,並最終,在第 6 步,兩個 goroutine 都將它們的手從通道里拿出來,這模擬了被鎖住的 goroutine 得到釋放。兩個 goroutine 現在都可以去做別的事情了。

無緩沖的 channel 創建格式:

make(chan Type)  // 等價於 make(chan Type, 0)

如果沒有指定緩沖區容量,那么該通道就是同步的,因此會阻塞到發送者准備好發送和接收者准備好接收。

例如:

package main

import (
	"fmt"
	"time"
)

func main() {
	// 創建無緩沖的 channel
	ch := make(chan int, 0)

	go func() {
		defer fmt.Println("子 go 程結束")
		for i := 0; i < 3; i++ {
			fmt.Println("子 go 程正在運行, i = ", i)
			ch <- i
		}
	}()
	time.Sleep(time.Second) // 延時一秒
	for i := 0; i < 3; i++ {
		// 從 ch 中接收數據, 並賦值給 num
		num := <-ch
		fmt.Println("num = ", num)
	}
	fmt.Println("main go程結束")
}

運行結果:

子 go 程正在運行, i =  0
num =  0
子 go 程正在運行, i =  1
子 go 程正在運行, i =  2
num =  1
num =  2
main go程結束

強調一下:

無緩沖 channel 的容量為0。

channel 至少應用於兩個 go 程中:一個讀、另一個寫。

具備同步能力。讀、寫同步。(比如 打電話)

有緩沖的channel - 異步通信

有緩沖的通道(buffered channel)是一種在被接收前能存儲一個或者多個數據值的通道。

這種類型的通道並不強制要求 goroutine 之間必須同時完成發送和接收。通道會阻塞發送和接收動作的條件也不同。

只有通道中沒有要接收的值時,接收動作才會阻塞。

只有通道沒有可用緩沖區容納被發送的值時,發送動作才會阻塞。

這導致有緩沖的通道和無緩沖的通道之間的一個很大的不同:無緩沖的通道保證進行發送和接收的 goroutine 會在同一時間進行數據交換;有緩沖的通道沒有這種保證。

使用有緩沖channel在goroutine之間同步的示例圖:

使用有緩沖channel在goroutine之間同步

  • 在第 1 步,右側的 goroutine 正在從通道接收一個值。
  • 在第 2 步,右側的這個 goroutine 獨立完成了接收值的動作,而左側的 goroutine 正在發送一個新值到通道里。
  • 在第 3 步,左側的 goroutine 還在向通道發送新值,而右側的 goroutine 正在從通道接收另外一個值。這個步驟里的兩個操作既不是同步的,也不會互相阻塞。
  • 最后,在第 4 步,所有的發送和接收都完成,而通道里還有幾個值,也有一些空間可以存更多的值。

有緩沖的 channel 創建格式:

make(chan Type, capacity)

如果給定了一個緩沖區容量,通道就是異步的。只要緩沖區有未使用空間用於發送數據,或還包含可以接收的數據,那么其通信就會無阻塞地進行。

請看以下代碼:

package main

import (
	"fmt"
	"time"
)

func main() {
	// 創建一個有緩沖的 channel
	ch := make(chan int, 3)  // 存滿 3 個元素之前不會阻塞

	// 查看一下 channel 的未被讀取的緩沖元素數量以及 channel 容量
	fmt.Printf("len(ch) = %d, cap(ch) = %d\n", len(ch), cap(ch))

	go func() {
		defer fmt.Println("子 go 程結束")
		for i := 0; i < 5; i++ {
			ch <- i
			fmt.Println("子 go 程正在運行, i = ", i)
		}
	}()

	time.Sleep(time.Second)

	for i := 0; i < 5; i++ {
		num := <-ch
		fmt.Println("num = ", num)
	}
	fmt.Println("main go 程結束")
}

運行結果:

len(ch) = 0, cap(ch) = 3
子 go 程正在運行, i =  0
子 go 程正在運行, i =  1
子 go 程正在運行, i =  2
num =  0
num =  1
num =  2
num =  3
子 go 程正在運行, i =  3
子 go 程正在運行, i =  4
子 go 程結束
num =  4
main go 程結束

強調一下:

有緩沖 channel 的容量大於 0。

channel 應用於兩個 go 程中:一個讀、另一個寫。

緩沖區可以進行數據存儲,存儲至容量上限才阻塞。

具備異步的能力,不需要同時操作 channel 緩沖區。(比如發短信)

關閉channel

如果發送者知道,沒有更多的值需要發送到 channel 的話,那么讓接收者也能及時知道沒有多余的值可接收將是有用的,因為接收者可以停止不必要的接收等待。

這可以通過內置的 close 函數來關閉 channel 實現。當我們確定不再向對端發送、接收數據時,我們可以關閉 channel。(一般關閉發送端)

對端可以判斷 channel 是否關閉:

if num, ok := <- ch; ok {
    // 對端沒有關閉,num 保存讀到的數據
} else {
    // 對端已經關閉,num 保存對應類型的零值
    
}

例如:

package main

import "fmt"

func main() {
	ch := make(chan int)

	go func() {
		for i := 0; i < 5; i++ {
			ch <- i
		}
		// 如果沒有 close(ch), 那么當程序打印完 0 1 2 3 4 時, 會因為沒有寫端 channel 造成死鎖
		close(ch)  // 寫端,寫完數據主動關閉 channel
	}()

    // 從 channel 中讀取數據,但是不知道讀多少次,我們可以判斷當 channel 關閉時意味着讀取數據完畢
	for true {
		// ok 為 true說明 channel 沒有關閉, 為 false 說明 channel 已經關閉
		if data, ok := <-ch; ok {
			fmt.Println("寫端沒有關閉,data = ", data)
		} else {
			fmt.Println("寫端關閉,data = ", data)
			break
		}
	}
	fmt.Println("結束.")
}

運行結果:

寫端沒有關閉,data =  0
寫端沒有關閉,data =  1
寫端沒有關閉,data =  2
寫端沒有關閉,data =  3
寫端沒有關閉,data =  4
寫端關閉,data =  0
結束.

我們也可以用 for range 獲取 channel 中的數據:

package main

import (
	"fmt"
	"time"
)

func main() {
	ch := make(chan int, 5)

	go func() {
		for i := 0; i < 5; i++ {
			ch <- i
		}
		// 如果沒有 close(ch), 那么當程序打印完 0 1 2 3 4 時, 會因為沒有寫端 channel 造成死鎖
		close(ch)  // 寫端,寫完數據主動關閉 channel
		fmt.Println("子 go 程結束")
	}()

	time.Sleep(time.Second)

	// 使用 for range 循環讀取 channel 的數據,注意這里前面只接收一個變量
	for num := range ch {
		fmt.Println(num)
	}

	fmt.Println("結束.")
}

運行結果:

子 go 程結束
0
1
2
3
4
結束.

強調一下:

  • channel 不像文件一樣需要經常去關閉,只有當你確實沒有任何發送數據了,或者你想顯式的結束 range 循環之類的,才去關閉 channel。簡單說就是數據沒發送完,不應該關閉 channel
  • 關閉 channel 后,無法向 channel 再發送數據(引發 panic 錯誤后導致接收立即返回零值)【panic: send on closed channel】
  • 寫端關閉 channel 后,可以繼續從 channel 接收數據
    • 如果 channel 中無數據,則讀到的為對應類型的零值(注意與無緩沖 channel 的區別)
    • 如果 channel 中有數據,則先讀該數據,讀完數據后,繼續讀則讀到的為對應類型的零值
  • 對於 nil channel,無論收發都會被阻塞。
  • 可以使用 for range 替代 ok 那種形式:
    for num := range ch{}  // 注意形式,不是 <-ch
    

單向 channel 及應用

默認情況下,通道 channel 是雙向的,也就是,既可以往里面發送數據也可以同里面接收數據。

但是,我們經常見一個通道作為參數進行傳遞而只希望對方是單向使用的,要么只讓它發送數據,要么只讓它接收數據,這時候我們可以指定通道的方向。

image

單向 channel 變量的聲明非常簡單,如下:

var ch1 chan int  // ch1 是一個正常的 channel,是雙向的
var ch2 chan<- float64  // ch2 是一個單向 channel,只能用於寫 float64 數據
var ch3 <-chan int  // ch3 是一個單向 channel,只能用於讀 int 數據
  • chan<- 表示數據進入管道,要把數據寫進管道,對於調用者就是輸出。
  • <-chan 表示數據從管道出來,對於調用者就是得到管道的數據,當然就是輸入。

可以將 channel 隱式轉換為單向隊列,只收或只發,不能將單向 channel 轉換為雙向 channel:

ch := make(chan int, 3)
var sendCh chan<- int = ch  // 只寫
var recvCh <-chan int  // 只讀

來看一下單向 channel 的簡單示例(記住了,channel 是傳引用):

package main

import "fmt"

// 只寫
func send(sendCh chan<- int) {
	sendCh <- 777
	close(sendCh)
}

// 只讀
func recv(recvCh <-chan int) {
	num := <-recvCh
	fmt.Println("num = ", num)
}

func main() {
	ch := make(chan int)
	go send(ch)
	recv(ch)
}

運行結果:

num =  777

生產者消費模型

生產者消費者模型分析

單向 channel 最典型的應用是: 生產者消費者模型.

所謂生產者消費者模型: 某個模塊(函數等)負責產生數據, 這些數據由另一個模塊來負責處理(此處的模塊是廣義的, 可以是類, 函數, 協程, 線程, 進程等). 產生數據的模塊, 就形象地稱為生產者; 而處理數據的模塊, 就稱為消費者.

單單抽象出生產者和消費者, 還夠不上是生產者消費者模型. 該模式還需要有一個緩沖區處於生產者和消費者之間, 作為一個中介. 生產者把數據放入緩沖區, 而消費者從緩沖區取出數據. 如下圖所示

生產者消費者模型

可以這樣理解, 假設你要寄一封信, 大致過程如下:

  1. 把信寫好 -- 相當於生產者制造數據
  2. 把信放入郵筒 -- 相當於生產者把數據放入緩沖區
  3. 郵遞員把信從郵筒取出 -- 相當於消費者把數據取出緩沖區
  4. 郵遞員把信拿去郵局做相應的處理 -- 相當於消費者處理數據

那么, 這個緩沖區有什么用呢? 為什么不讓生產者直接調用消費者的某個函數, 直接把數據傳遞過去, 而去設置一個緩沖區呢?

緩沖區的好處大概如下:

1: 解耦 ( 降低 生產者 和 消費者 之間的耦合度 )

假設生產者和消費者分別是兩個類. 如果讓生產者直接調用消費者的某個方法, 那么生產者對於消費者就會產生依賴(也就是耦合). 將來如果消費者的代碼發生變化, 可能會直接影響到生產者. 而如果兩者都依賴某個緩沖區, 兩者之間不直接依賴, 耦合度也就相應降低了.

依然用寄信的例子簡單說一下, 假設生產者就是你, 你負責寫信, 如果沒有郵筒(即緩沖區), 你就需要直接把信給郵遞員(消費者). 但是, 過了幾個月, 郵遞員換人了, 你想要寄信就必須再認識新的郵遞員, 你剛和新的郵遞員熟悉之后, 又換了一個郵遞員, 你又要重新認識... 這就顯得很麻煩, 就是想寄個信而已, 不想認識那么多郵遞員...

但是如果有郵筒(緩沖區)呢, 無論郵遞員怎么更換, 這個與你無關, 我依然是把信放入郵筒就可以了. 這樣一來, 就簡單多了.

2: 提高並發能力 ( 生產者與消費者數量不對等時, 能保持正常通信 )

生產者直接調用消費者的某個方法, 還有另一個弊端

由於函數調用是同步的(或者叫阻塞的), 在消費者的方法沒有返回之前, 生產者只好一直等在那邊. 萬一消費者處理數據很慢, 生產者只能白白浪費時間.

使用了生產者/消費者模式之后, 生產者和消費者可以是兩個獨立的並發主體.

生產者把制造出來的數據放入緩沖區, 就可以再去生產下一個數據. 基本上不用依賴消費者的處理速度.

其實最初這個生產者消費者模式, 主要就是用來處理並發問題的.

從寄信的例子來看, 如果沒有郵筒, 你得拿着信傻站在路口等郵遞員過來收(相當於生產者阻塞); 又或者郵遞員得挨家挨戶問, 誰要寄信(相當於消費者輪詢).

3: 緩存 ( 生產者與消費者數據處理速度不一致時, 暫存數據 )

如果生產者制造數據的速度時快時慢, 緩沖區的好處就體現出來了.

當數據制造快的時候, 消費者來不及處理, 未處理的數據可以暫時存在緩沖區中. 等生產者的制造速度慢下來, 消費者再慢慢處理掉.

再拿寄信的例子舉例, 假設郵遞員一次只能帶走1000封信. 萬一某次碰上情人節送賀卡, 需要寄出的信超過1000封, 這時候郵筒這個緩沖區就派上用場了. 郵遞員把來不及帶走的信暫存在郵筒中, 等下次過來時再拿走.

生產者消費者模型實現

先來看一下無緩沖的例子

package main

import "fmt"

// 生產者
func producer(ch chan<- int) {
	for i := 0; i < 5; i++ {
		fmt.Println("生產者寫入數據, num = ", i)
		ch <- i
	}
	close(ch)
}

// 消費者
func consumer(ch <-chan int) {
	for num := range ch {
		fmt.Println("消費者拿到數據, num = ", num)
	}
}

func main() {
	// 無緩沖 channel
	ch := make(chan int)
	go producer(ch)  // 子 go 程,生產者
	consumer(ch)  // 主 go 程,消費者
}

運行結果:

生產者寫入數據, num =  0
生產者寫入數據, num =  1
消費者拿到數據, num =  0
消費者拿到數據, num =  1
生產者寫入數據, num =  2
生產者寫入數據, num =  3
消費者拿到數據, num =  2
消費者拿到數據, num =  3
生產者寫入數據, num =  4
消費者拿到數據, num =  4

再來看一下有緩沖的例子 兩者對比結果

package main

import "fmt"

// 生產者
func producer(ch chan<- int) {
	for i := 0; i < 5; i++ {
		fmt.Println("生產者寫入數據, num = ", i)
		ch <- i
	}
	close(ch)
}

// 消費者
func consumer(ch <-chan int) {
	for num := range ch {
		fmt.Println("消費者拿到數據, num = ", num)
	}
}

func main() {
	// 有緩沖 channel
	ch := make(chan int, 2)
	go producer(ch)  // 子 go 程,生產者
	consumer(ch)  // 主 go 程,消費者
}

運行結果:

生產者寫入數據, num =  0
生產者寫入數據, num =  1
生產者寫入數據, num =  2
生產者寫入數據, num =  3
消費者拿到數據, num =  0
消費者拿到數據, num =  1
消費者拿到數據, num =  2
消費者拿到數據, num =  3
生產者寫入數據, num =  4
消費者拿到數據, num =  4

簡單說明

首先創建一個雙向的 channel, 然后開啟一個新的 goroutine, 把雙向通道作為參數傳遞到 producer 方法中, 同時轉成只寫通道. 子 go 程開始執行循環, 向只寫通道中添加數據, 這就是生產者.

主 go 程直接調用 consumer 方法, 該方法將雙向通道轉成只讀通道, 通過循環每次從通道中讀取數據, 這就是消費者.

注意, channel 作為參數傳遞, 是引用傳遞.

生產者消費者 - 模擬訂單

在實際的開發中, 生產者消費者模式應用也非常的廣泛.

例如, 在電商網站中, 訂單處理, 就是非常典型的生產者消費者模式.

當很多用戶單擊下訂單按鈕后, 訂單生產的數據全部放到緩沖區(隊列)中, 然后消費者將隊列中的數據取出來發送至倉庫管理等系統.

通過生產者消費者模式, 將訂單系統與倉庫管理系統隔離開, 且用戶可以隨時下單(生產數據). 如果訂單系統直接調用倉庫系統, 那么用戶單擊下訂單按鈕后, 要等到倉庫系統的結果返回, 這樣速度很慢.

接下來我們就來模擬一下訂單處理的過程.

package main

import "fmt"

type OrderInfo struct {
	id int
}

func producer2(out chan<- OrderInfo) {  // 生成訂單 -- 生產者
	for i:=0; i < 10; i++ {  // 循環生成10個訂單
		order := OrderInfo{id: i+1}
		fmt.Println("生成的訂單ID: ", order.id)
		out <- order
	}
	close(out)  // 寫完, 關閉channel

}
func consumer2(in <-chan OrderInfo) {  // 處理訂單 -- 消費者
	for order := range in {  // 從channel取出訂單
		fmt.Println("訂單ID為: ", order.id)  // 模擬處理訂單
	}
}

func main() {
	ch := make(chan OrderInfo, 5)
	go producer2(ch)
	consumer2(ch)
}

簡單說明: OrderInfo 為訂單信息, 這里為了簡單只定義了一個訂單編號屬性, 然后生產者模擬生成10個訂單, 消費者對產生的訂單進行處理.

定時器

time.Timer

Timer 是一個定時器. 代表未來的一個單一事件, 你可以告訴 Timer 你要等待多長時間.

type Timer struct {
    C <- chan Time
    r runtimeTimer
}

它提供一個channel, 在定時時間到達之前, 沒有數據寫入 Timer.C 會一直阻塞. 直到定時時間到, 系統會自動向 Timer.C 這個channel中寫入當前時間, 阻塞即被解除.

定時器的啟動

示例代碼:

package main

import (
	"fmt"
	"time"
)

func main() {
	fmt.Println("當前時間: ", time.Now())

	// 創建定時器, 指定定時時長
	myTimer := time.NewTimer(time.Second * 2)
	// 定時到達后, 系統會自動向定時器的成員 C 寫入系統當前系統時間
	//讀取 myTimer.C 得到定時后的系統時間, 並完成一次chan的讀操作.
	nowTime := <- myTimer.C
	fmt.Println("當前時間: ", nowTime)
}

3 種定時方法

1. Sleep
time.Sleep(time.Second)

2. Time.C
fmt.Println("當前時間: ", time.Now())
myTimer := time.NewTimer(time.Second * 2)
nowTime := <- myTimer.C
fmt.Println("現在時間: ", nowTime)

3. time.After
fmt.Println("當前時間: ", time.Now())
nowTime := <- time.After(time.Second * 2)
fmt.Println("現在時間: ", nowTime)

定時器的停止

package main

import (
	"fmt"
	"time"
)

func main(){
    myTimer := time.NewTimer(time.Second * 3)  // 創建定時器
    go func() {
       <- myTimer.C
       fmt.Println("子go程, 定時完畢")
    }()
    
    myTimer.Stop()  // 設置定時器停止
    for {
        ;
    }
}

死循環只是為了方便查看結果.

定時器的重置

package main

import (
	"fmt"
	"time"
)

func main() {
    myTimer := time.NewTimer(time.Second * 10)
    myTimer.Reset(time.Second * 2)  // 重置定時時長為 2 秒
    go func(){
        <- myTimer.C
        fmt.Println("子go程, 定時完畢")
    }()
    
    for {
        ;
    }
}
  • 創建定時器: myTimer := time.NewTimer(time.Second * 2)
  • 停止定時器: myTimer.Stop() [此時 <- myTimer.C 會阻塞]
  • 重置定時器: myTimer.Reset(time.Second * 2)

周期定時器 Time.Ticker

Ticker是一個周期觸發定時的計時器, 它會按照一個時間間隔往channel發送系統當前時間, 而channel的接受者可以以固定的時間間隔從channel中讀取.

type Ticker struct {
    C <- chan Time
    r runtimeTimer
}
package main

import (
	"fmt"
	"time"
)

func main() {
	myTicker := time.NewTicker(time.Second)  // 定義一個周期定時器

	go func() {
		for {
			nowTime := <- myTicker.C
			fmt.Println("現在時間: ", nowTime)
		}
	}()
	// 死循環, 特地不讓main goroutine結束
	for  {
		;
	}
}
package main

import (
	"fmt"
	"time"
)

func main(){
	quit := make(chan bool)  // 創建一個判斷是否終止的channel
	myTicker := time.NewTicker(time.Second)  // 定義一個周期定時器
	go func() {
		i := 0
		for  {
			nowTime := <- myTicker.C
			i++
			fmt.Println("現在時間: ", nowTime)
			if i == 5 {
				quit <- true  // 解除 主go程阻塞
			}
		}
	}()
	<- quit  // 在子go程循環獲取 <- myTicker.C 期間, 一直阻塞
}

李培冠博客

歡迎訪問我的個人網站:

李培冠博客:lpgit.com


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM