Go語言 協(xié)程、延遲函數(shù)調(diào)用、以及恐慌和恢復(fù)

2023-02-16 17:36 更新

此篇文章將介紹協(xié)程和延遲函數(shù)調(diào)用。協(xié)程和延遲函數(shù)調(diào)用是Go中比較獨(dú)特的兩個(gè)特性。 恐慌和恢復(fù)也將在此篇文章中得到簡單介紹。本文并非全面地對這些特性進(jìn)行介紹,后面的其它文章會陸續(xù)補(bǔ)全本文的未介紹的內(nèi)容。

協(xié)程(goroutine)

現(xiàn)代CPU一般含有多個(gè)核,并且一個(gè)核可能支持多線程。換句話說,現(xiàn)代CPU可以同時(shí)執(zhí)行多條指令流水線。 為了將CPU的能力發(fā)揮到極致,我們常常需要使我們的程序支持并發(fā)(concurrent)計(jì)算。

并發(fā)計(jì)算是指若干計(jì)算可能在某些時(shí)間片段內(nèi)同時(shí)運(yùn)行的情形。 下面這兩張圖描繪了兩種并發(fā)計(jì)算的場景。在此圖中,A和B表示兩個(gè)計(jì)算。 在第一種情形中,兩個(gè)計(jì)算只在某些時(shí)間片段同時(shí)運(yùn)行。 第二種情形稱為并行(parallel)計(jì)算。在并行計(jì)算中,多個(gè)計(jì)算在任何時(shí)間點(diǎn)都在同時(shí)運(yùn)行。并行計(jì)算屬于特殊的并發(fā)計(jì)算。

并發(fā)和并行

并發(fā)計(jì)算可能發(fā)生在同一個(gè)程序中、同一臺電腦上、或者同一個(gè)網(wǎng)絡(luò)中。 在《Go語言101》中,我們只談及發(fā)生在同一個(gè)程序中的并發(fā)計(jì)算。 在Go編程中,協(xié)程是創(chuàng)建計(jì)算的唯一途徑。

協(xié)程有時(shí)也被稱為綠色線程。綠色線程是由程序的運(yùn)行時(shí)(runtime)維護(hù)的線程。一個(gè)綠色線程的內(nèi)存開銷和情景轉(zhuǎn)換(context switching)時(shí)耗比一個(gè)系統(tǒng)線程常常小得多。 只要內(nèi)存充足,一個(gè)程序可以輕松支持上萬個(gè)并發(fā)協(xié)程。

Go不支持創(chuàng)建系統(tǒng)線程,所以協(xié)程是一個(gè)Go程序內(nèi)部唯一的并發(fā)實(shí)現(xiàn)方式。

每個(gè)Go程序啟動(dòng)的時(shí)候只有一個(gè)對用戶可見的協(xié)程,我們稱之為主協(xié)程。 一個(gè)協(xié)程可以開啟更多其它新的協(xié)程。在Go中,開啟一個(gè)新的協(xié)程是非常簡單的。 我們只需在一個(gè)函數(shù)調(diào)用之前使用一個(gè)go關(guān)鍵字,即可讓此函數(shù)調(diào)用運(yùn)行在一個(gè)新的協(xié)程之中。 當(dāng)此函數(shù)調(diào)用退出后,這個(gè)新的協(xié)程也隨之結(jié)束了。我們可以稱此函數(shù)調(diào)用為一個(gè)協(xié)程調(diào)用(或者為此協(xié)程的啟動(dòng)調(diào)用)。 一個(gè)協(xié)程調(diào)用的所有返回值(如果存在的話)必須被全部舍棄。

在下面的例子程序中,主協(xié)程創(chuàng)建了兩個(gè)新的協(xié)程。在此例中,time.Duration是一個(gè)在time標(biāo)準(zhǔn)庫包中定義的類型。 此類型的底層類型為內(nèi)置類型int64。 底層類型這個(gè)概念將在下一篇文章中介紹。

package main

import (
	"log"
	"math/rand"
	"time"
)

func SayGreetings(greeting string, times int) {
	for i := 0; i < times; i++ {
		log.Println(greeting)
		d := time.Second * time.Duration(rand.Intn(5)) / 2
		time.Sleep(d) // 睡眠片刻(隨機(jī)0到2.5秒)
	}
}

func main() {
	rand.Seed(time.Now().UnixNano())
	log.SetFlags(0)
	go SayGreetings("hi!", 10)
	go SayGreetings("hello!", 10)
	time.Sleep(2 * time.Second)
}

非常簡單!我們編寫了一個(gè)并發(fā)程序! 此程序在運(yùn)行的時(shí)候在某一時(shí)刻將很可能會有三個(gè)協(xié)程并存。 運(yùn)行之,可能會得到如下的結(jié)果(也可能是其它結(jié)果):

hi!
hello!
hello!
hello!
hello!
hi!

當(dāng)一個(gè)程序的主協(xié)程退出后,此程序也就退出了,即使還有一些其它協(xié)程在運(yùn)行。

和前面的幾篇文章不同,上面的例子程序使用了?log?標(biāo)準(zhǔn)庫而不是?fmt?標(biāo)準(zhǔn)庫中的?Println?函數(shù)。 原因是?log?標(biāo)準(zhǔn)庫中的打印函數(shù)是經(jīng)過了同步處理的(下一節(jié)將解釋什么是并發(fā)同步),而?fmt?標(biāo)準(zhǔn)庫中的打印函數(shù)卻沒有被同步。 如果我們在上例中使用?fmt?標(biāo)準(zhǔn)庫中的?Println?函數(shù),則不同協(xié)程的打印可能會交織在一起。(雖然對此例來說,交織的概率很低。)

并發(fā)同步(concurrency synchronization)

不同的并發(fā)計(jì)算可能共享一些資源,其中共享內(nèi)存資源最為常見。 在一個(gè)并發(fā)程序中,常常會發(fā)生下面的情形:

  • 在一個(gè)計(jì)算向一段內(nèi)存寫數(shù)據(jù)的時(shí)候,另一個(gè)計(jì)算從此內(nèi)存段讀數(shù)據(jù),結(jié)果導(dǎo)致讀出的數(shù)據(jù)的完整性得不到保證。
  • 在一個(gè)計(jì)算向一段內(nèi)存寫數(shù)據(jù)的時(shí)候,另一個(gè)計(jì)算也向此段內(nèi)存寫數(shù)據(jù),結(jié)果導(dǎo)致被寫入的數(shù)據(jù)的完整性得不到保證。

這些情形被稱為數(shù)據(jù)競爭(data race)。并發(fā)編程的一大任務(wù)就是要調(diào)度不同計(jì)算,控制它們對資源的訪問時(shí)段,以使數(shù)據(jù)競爭的情況不會發(fā)生。 此任務(wù)常稱為并發(fā)同步(或者數(shù)據(jù)同步)。Go支持幾種并發(fā)同步技術(shù),這些并發(fā)同步技術(shù)將在后面的章節(jié)中逐一介紹。

并發(fā)編程中的其它任務(wù)包括:

  • 決定需要開啟多少計(jì)算;
  • 決定何時(shí)開啟、阻塞、解除阻塞和結(jié)束哪些計(jì)算;
  • 決定如何在不同的計(jì)算中分擔(dān)工作負(fù)載。

上一節(jié)中這個(gè)并發(fā)程序是有缺陷的。我們本期望每個(gè)新創(chuàng)建的協(xié)程打印出10條問候語,但是主協(xié)程(和程序)在這20條問候語還未都打印出來的時(shí)候就退出了。 如何確保主協(xié)程在這20條問候語都打印完畢之后才退出呢?我們必須使用某種并發(fā)同步技術(shù)來達(dá)成這一目標(biāo)。

Go支持幾種并發(fā)同步技術(shù)。 其中, 通道是最獨(dú)特和最常用的。 但是,為了簡單起見,這里我們將使用?sync?標(biāo)準(zhǔn)庫包中的?WaitGroup?來同步上面這個(gè)程序中的主協(xié)程和兩個(gè)新創(chuàng)建的協(xié)程。

?WaitGroup?類型有三個(gè)方法(特殊的函數(shù),將在以后的文章中詳解):?Add?、?Done?和?Wait?。此類型將在后面的某篇文章中詳細(xì)解釋,目前我們可以簡單地認(rèn)為:

  • ?Add?方法用來注冊新的需要完成的任務(wù)數(shù)。
  • ?Done?方法用來通知某個(gè)任務(wù)已經(jīng)完成了。
  • 一個(gè)?Wait?方法調(diào)用將阻塞(等待)到所有任務(wù)都已經(jīng)完成之后才繼續(xù)執(zhí)行其后的語句。

示例:

package main

import (
	"log"
	"math/rand"
	"time"
	"sync"
)

var wg sync.WaitGroup

func SayGreetings(greeting string, times int) {
	for i := 0; i < times; i++ {
		log.Println(greeting)
		d := time.Second * time.Duration(rand.Intn(5)) / 2
		time.Sleep(d)
	}
	wg.Done() // 通知當(dāng)前任務(wù)已經(jīng)完成。
}

func main() {
	rand.Seed(time.Now().UnixNano())
	log.SetFlags(0)
	wg.Add(2) // 注冊兩個(gè)新任務(wù)。
	go SayGreetings("hi!", 10)
	go SayGreetings("hello!", 10)
	wg.Wait() // 阻塞在這里,直到所有任務(wù)都已完成。
}

運(yùn)行這個(gè)修改后的程序,我們將會發(fā)現(xiàn)所有的20條問候語都將在程序退出之前打印出來。

協(xié)程的狀態(tài)

從上面這個(gè)的例子,我們可以看到一個(gè)活動(dòng)中的協(xié)程可以處于兩個(gè)狀態(tài):運(yùn)行狀態(tài)阻塞狀態(tài)。一個(gè)協(xié)程可以在這兩個(gè)狀態(tài)之間切換。 比如上例中的主協(xié)程在調(diào)用?wg.Wait?方法的時(shí)候,將從運(yùn)行狀態(tài)切換到阻塞狀態(tài);當(dāng)兩個(gè)新協(xié)程完成各自的任務(wù)后,主協(xié)程將從阻塞狀態(tài)切換回運(yùn)行狀態(tài)。

下面的圖片顯示了一個(gè)協(xié)程的生命周期。

協(xié)程狀態(tài)

注意,一個(gè)處于睡眠中的(通過調(diào)用?time.Sleep?)或者在等待系統(tǒng)調(diào)用返回的協(xié)程被認(rèn)為是處于運(yùn)行狀態(tài),而不是阻塞狀態(tài)。

當(dāng)一個(gè)新協(xié)程被創(chuàng)建的時(shí)候,它將自動(dòng)進(jìn)入運(yùn)行狀態(tài),一個(gè)協(xié)程只能從運(yùn)行狀態(tài)而不能從阻塞狀態(tài)退出。 如果因?yàn)槟撤N原因而導(dǎo)致某個(gè)協(xié)程一直處于阻塞狀態(tài),則此協(xié)程將永遠(yuǎn)不會退出。 除了極個(gè)別的應(yīng)用場景,在編程時(shí)我們應(yīng)該盡量避免出現(xiàn)這樣的情形。

一個(gè)處于阻塞狀態(tài)的協(xié)程不會自發(fā)結(jié)束阻塞狀態(tài),它必須被另外一個(gè)協(xié)程通過某種并發(fā)同步方法來被動(dòng)地結(jié)束阻塞狀態(tài)。 如果一個(gè)運(yùn)行中的程序當(dāng)前所有的協(xié)程都出于阻塞狀態(tài),則這些協(xié)程將永遠(yuǎn)阻塞下去,程序?qū)⒈灰暈樗梨i了。 當(dāng)一個(gè)程序死鎖后,官方標(biāo)準(zhǔn)編譯器的處理是讓這個(gè)程序崩潰。

比如下面這個(gè)程序?qū)⒃谶\(yùn)行兩秒鐘后崩潰。

package main

import (
	"sync"
	"time"
)

var wg sync.WaitGroup

func main() {
	wg.Add(1)
	go func() {
		time.Sleep(time.Second * 2)
		wg.Wait() // 阻塞在此
	}()
	wg.Wait() // 阻塞在此
}

它的輸出:

fatal error: all goroutines are asleep - deadlock!

...

以后我們將學(xué)習(xí)到更多可以讓一個(gè)協(xié)程進(jìn)入到阻塞狀態(tài)的操作。

協(xié)程的調(diào)度

并非所有處于運(yùn)行狀態(tài)的協(xié)程都在執(zhí)行。在任一時(shí)刻,只能最多有和邏輯CPU數(shù)目一樣多的協(xié)程在同時(shí)執(zhí)行。 我們可以調(diào)用runtime.NumCPU函數(shù)來查詢當(dāng)前程序可利用的邏輯CPU數(shù)目。 每個(gè)邏輯CPU在同一時(shí)刻只能最多執(zhí)行一個(gè)協(xié)程。Go運(yùn)行時(shí)(runtime)必須讓邏輯CPU頻繁地在不同的處于運(yùn)行狀態(tài)的協(xié)程之間切換,從而每個(gè)處于運(yùn)行狀態(tài)的協(xié)程都有機(jī)會得到執(zhí)行。 這和操作系統(tǒng)執(zhí)行系統(tǒng)線程的原理是一樣的。

下面這張圖顯示了一個(gè)協(xié)程的更詳細(xì)的生命周期。在此圖中,運(yùn)行狀態(tài)被細(xì)分成了多個(gè)子狀態(tài)。 一個(gè)處于排隊(duì)子狀態(tài)的協(xié)程等待著進(jìn)入執(zhí)行子狀態(tài)。一個(gè)處于執(zhí)行子狀態(tài)的協(xié)程在被執(zhí)行一會兒(非常短的時(shí)間片)之后將進(jìn)入排隊(duì)子狀態(tài)。

協(xié)程詳細(xì)狀態(tài)

請注意,為了解釋的簡單性,在以后其它的《Go語言101》文章中,上圖中所示的子狀態(tài)將不會再提及。 重申一下,睡眠和等待系統(tǒng)調(diào)用返回子狀態(tài)被認(rèn)為是運(yùn)行狀態(tài),而不是阻塞狀態(tài)。

標(biāo)準(zhǔn)編譯器采納了一種被稱為M-P-G模型的算法來實(shí)現(xiàn)協(xié)程調(diào)度。 其中,M表示系統(tǒng)線程,P表示邏輯處理器(并非上述的邏輯CPU),G表示協(xié)程。 大多數(shù)的調(diào)度工作是通過邏輯處理器(P)來完成的。 邏輯處理器像一個(gè)監(jiān)工一樣通過將不同的處于運(yùn)行狀態(tài)協(xié)程(G)交給不同的系統(tǒng)線程(M)來執(zhí)行。 一個(gè)協(xié)程在同一時(shí)刻只能在一個(gè)系統(tǒng)線程中執(zhí)行。一個(gè)執(zhí)行中的協(xié)程運(yùn)行片刻后將自發(fā)地脫離讓出一個(gè)系統(tǒng)線程,從而使得其它處于等待子狀態(tài)的協(xié)程得到執(zhí)行機(jī)會。

在運(yùn)行時(shí)刻,我們可以調(diào)用runtime.GOMAXPROCS函數(shù)來獲取和設(shè)置邏輯處理器的數(shù)量。 對于官方標(biāo)準(zhǔn)編譯器,在Go 1.5之前,默認(rèn)初始邏輯處理器的數(shù)量為1;自從Go 1.5之后,默認(rèn)初始邏輯處理器的數(shù)量和邏輯CPU的數(shù)量一致。 此新的默認(rèn)設(shè)置在大多數(shù)情況下是最佳選擇。但是對于某些文件操作十分頻繁的程序,設(shè)置一個(gè)大于?runtime.NumCPU()?的?GOMAXPROCS?值可能是有好處的。

我們也可以通過設(shè)置?GOMAXPROCS?環(huán)境變量來設(shè)置一個(gè)Go程序的初始邏輯處理器數(shù)量。

延遲函數(shù)調(diào)用(deferred function call)

在Go中,一個(gè)函數(shù)調(diào)用可以跟在一個(gè)?defer?關(guān)鍵字后面,成為一個(gè)延遲函數(shù)調(diào)用。 此?defer?關(guān)鍵字和此延遲函數(shù)調(diào)用一起形成一個(gè)延遲調(diào)用語句。 和協(xié)程調(diào)用類似,被延遲的函數(shù)調(diào)用的所有返回值(如果存在)必須全部被舍棄。

當(dāng)一個(gè)延遲調(diào)用語句被執(zhí)行時(shí),其中的延遲函數(shù)調(diào)用不會立即被執(zhí)行,而是被推入由當(dāng)前協(xié)程維護(hù)的一個(gè)延遲調(diào)用隊(duì)列(一個(gè)后進(jìn)先出隊(duì)列)。 當(dāng)一個(gè)函數(shù)調(diào)用返回(此時(shí)可能尚未完全退出)并進(jìn)入它的退出階段后,所有在執(zhí)行此函數(shù)調(diào)用的過程中已經(jīng)被推入延遲調(diào)用隊(duì)列的調(diào)用將被按照它們被推入的順序逆序被彈出隊(duì)列并執(zhí)行。 當(dāng)所有這些延遲調(diào)用執(zhí)行完畢后,此函數(shù)調(diào)用也就完全退出了。

下面這個(gè)例子展示了如何使用延遲調(diào)用函數(shù)。

package main

import "fmt"

func main() {
	defer fmt.Println("The third line.")
	defer fmt.Println("The second line.")
	fmt.Println("The first line.")
}

輸出結(jié)果:

The first line.
The second line.
The third line.

下面是另一個(gè)略微復(fù)雜一點(diǎn)的使用了延遲調(diào)用的例子程序。此程序?qū)凑兆匀粩?shù)的順序打印出0到9十個(gè)數(shù)字。

package main

import "fmt"

func main() {
	defer fmt.Println("9")
	fmt.Println("0")
	defer fmt.Println("8")
	fmt.Println("1")
	if false {
		defer fmt.Println("not reachable")
	}
	defer func() {
		defer fmt.Println("7")
		fmt.Println("3")
		defer func() {
			fmt.Println("5")
			fmt.Println("6")
		}()
		fmt.Println("4")
	}()
	fmt.Println("2")
	return
	defer fmt.Println("not reachable")
}

一個(gè)延遲調(diào)用可以修改包含此延遲調(diào)用的最內(nèi)層函數(shù)的返回值

一個(gè)例子:

package main

import "fmt"

func Triple(n int) (r int) {
	defer func() {
		r += n // 修改返回值
	}()

	return n + n // <=> r = n + n; return
}

func main() {
	fmt.Println(Triple(5)) // 15
}

延遲函數(shù)調(diào)用的必要性和好處

事實(shí)上,上面的幾個(gè)使用了延遲函數(shù)調(diào)用的例子中的延遲函數(shù)調(diào)用并非絕對必要。 但是延遲調(diào)用對于下面將要介紹的恐慌/恢復(fù)特性是必要的。

另外延遲函數(shù)調(diào)用可以幫助我們寫出更整潔和更魯棒的代碼。我們可以在后面的更多關(guān)于延遲調(diào)用一文中讀到這樣的例子。

協(xié)程和延遲調(diào)用的實(shí)參的估值時(shí)刻

一個(gè)延遲調(diào)用的實(shí)參是在此調(diào)用對應(yīng)的延遲調(diào)用語句被執(zhí)行時(shí)被估值的。 或者說,它們是在此延遲調(diào)用被推入延遲調(diào)用隊(duì)列時(shí)被估值的。 這些被估值的結(jié)果將在以后此延遲調(diào)用被執(zhí)行的時(shí)候使用。

一個(gè)匿名函數(shù)體內(nèi)的表達(dá)式是在此函數(shù)被執(zhí)行的時(shí)候才會被逐漸估值的,不管此函數(shù)是被普通調(diào)用還是延遲/協(xié)程調(diào)用。

一個(gè)例子:

package main

import "fmt"

func main() {
	func() {
		for i := 0; i < 3; i++ {
			defer fmt.Println("a:", i)
		}
	}()
	fmt.Println()
	func() {
		for i := 0; i < 3; i++ {
			defer func() {
				fmt.Println("b:", i)
			}()
		}
	}()
}

運(yùn)行之,將得到如下結(jié)果:

a: 2
a: 1
a: 0

b: 3
b: 3
b: 3

第一個(gè)匿名函數(shù)中的循環(huán)打印出?2?、?1?和?0?這個(gè)序列,但是第二個(gè)匿名函數(shù)中的循環(huán)打印出三個(gè)?3?。 因?yàn)榈谝粋€(gè)循環(huán)中的?i?是在?fmt.Println?函數(shù)調(diào)用被推入延遲調(diào)用隊(duì)列的時(shí)候估的值,而第二個(gè)循環(huán)中的?i?是在第二個(gè)匿名函數(shù)調(diào)用的退出階段估的值(此時(shí)循環(huán)變量?i?的值已經(jīng)變?yōu)?3?)。

我們可以對第二個(gè)循環(huán)略加修改(使用兩種方法),使得它和第一個(gè)循環(huán)打印出相同的結(jié)果。

		for i := 0; i < 3; i++ {
			defer func(i int) {
				// 此i為形參i,非實(shí)參循環(huán)變量i。
				fmt.Println("b:", i)
			}(i)
		}

或者

		for i := 0; i < 3; i++ {
			i := i // 在下面的調(diào)用中,左i遮擋了右i。
			       // <=> var i = i
			defer func() {
				// 此i為上面的左i,非循環(huán)變量i。
				fmt.Println("b:", i)
			}()
		}

同樣的估值時(shí)刻規(guī)則也適用于協(xié)程調(diào)用。下面這個(gè)例子程序?qū)⒋蛴〕?span style="background-color: rgb(249, 242, 244); color: rgb(199, 37, 78); font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace; font-size: inherit; white-space: nowrap;">123 789。

package main

import "fmt"
import "time"

func main() {
	var a = 123
	go func(x int) {
		time.Sleep(time.Second)
		fmt.Println(x, a) // 123 789
	}(a)

	a = 789

	time.Sleep(2 * time.Second)
}

順便說一句,使用?time.Sleep?調(diào)用來做并發(fā)同步不是一個(gè)好的方法。 如果上面這個(gè)程序運(yùn)行在一個(gè)滿負(fù)荷運(yùn)行的電腦上,此程序可能在新啟動(dòng)的協(xié)程可能還未得到執(zhí)行機(jī)會的時(shí)候就已經(jīng)退出了。 在正式的項(xiàng)目中,我們應(yīng)該使用并發(fā)同步技術(shù)一文中列出的方法來實(shí)現(xiàn)并發(fā)同步。

恐慌(panic)和恢復(fù)(recover)

Go不支持異常拋出和捕獲,而是推薦使用返回值顯式返回錯(cuò)誤。 不過,Go支持一套和異常拋出/捕獲類似的機(jī)制。此機(jī)制稱為恐慌/恢復(fù)(panic/recover)機(jī)制。

我們可以調(diào)用內(nèi)置函數(shù)?panic?來產(chǎn)生一個(gè)恐慌以使當(dāng)前協(xié)程進(jìn)入恐慌狀況。

進(jìn)入恐慌狀況是另一種使當(dāng)前函數(shù)調(diào)用開始返回的途徑。 一旦一個(gè)函數(shù)調(diào)用產(chǎn)生一個(gè)恐慌,此函數(shù)調(diào)用將立即進(jìn)入它的退出階段。

通過在一個(gè)延遲函數(shù)調(diào)用之中調(diào)用內(nèi)置函數(shù)?recover?,當(dāng)前協(xié)程中的一個(gè)恐慌可以被消除,從而使得當(dāng)前協(xié)程重新進(jìn)入正常狀況。

如果一個(gè)協(xié)程在恐慌狀況下退出,它將使整個(gè)程序崩潰。

內(nèi)置函數(shù)panicrecover的聲明原型如下:

func panic(v interface{})
func recover() interface{}

接口(interface)類型和接口值將在以后的文章接口中詳解。 目前,我們可以暫時(shí)將空接口類型?interface{}?視為很多其它語言中的?any?或者?Object?類型。 換句話說,在一個(gè)?panic?函數(shù)調(diào)用中,我們可以傳任何實(shí)參值。

一個(gè)?recover?函數(shù)的返回值為其所恢復(fù)的恐慌在產(chǎn)生時(shí)被一個(gè)?panic?函數(shù)調(diào)用所消費(fèi)的參數(shù)。

下面這個(gè)例子展示了如何產(chǎn)生一個(gè)恐慌和如何消除一個(gè)恐慌。

package main

import "fmt"

func main() {
	defer func() {
		fmt.Println("正常退出")
	}()
	fmt.Println("嗨!")
	defer func() {
		v := recover()
		fmt.Println("恐慌被恢復(fù)了:", v)
	}()
	panic("拜拜!") // 產(chǎn)生一個(gè)恐慌
	fmt.Println("執(zhí)行不到這里")
}

它的輸出結(jié)果:

嗨!
恐慌被恢復(fù)了: 拜拜!
正常退出

下面的例子在一個(gè)新協(xié)程里面產(chǎn)生了一個(gè)恐慌,并且此協(xié)程在恐慌狀況下退出,所以整個(gè)程序崩潰了。

package main

import (
	"fmt"
	"time"
)

func main() {
	fmt.Println("hi!")

	go func() {
		time.Sleep(time.Second)
		panic(123)
	}()

	for {
		time.Sleep(time.Second)
	}
}

運(yùn)行之,輸出如下:

hi!
panic: 123

goroutine 5 [running]:
...

Go運(yùn)行時(shí)(runtime)會在若干情形下產(chǎn)生恐慌,比如一個(gè)整數(shù)被0除的時(shí)候。下面這個(gè)程序?qū)⒈罎⑼顺觥?/p>

package main

func main() {
	a, b := 1, 0
	_ = a/b
}

它的輸出:

panic: runtime error: integer divide by zero

goroutine 1 [running]:
...

一般說來,恐慌用來表示正常情況下不應(yīng)該發(fā)生的邏輯錯(cuò)誤。 如果這樣的一個(gè)錯(cuò)誤在運(yùn)行時(shí)刻發(fā)生了,則它肯定是由于某個(gè)bug引起的。 另一方面,非邏輯錯(cuò)誤是現(xiàn)實(shí)中難以避免的錯(cuò)誤,它們不應(yīng)該導(dǎo)致恐慌。 我們必須正確地對待和處理非邏輯錯(cuò)誤。

更多可能由Go運(yùn)行時(shí)產(chǎn)生的恐慌將在以后其它文章中提及。

以后,我們可以了解一些恐慌/恢復(fù)用例更多關(guān)于恐慌/恢復(fù)機(jī)制的細(xì)節(jié)。

一些致命性錯(cuò)誤不屬于恐慌

對于官方標(biāo)準(zhǔn)編譯器來說,很多致命性錯(cuò)誤(比如棧溢出和內(nèi)存不足)不能被恢復(fù)。它們一旦產(chǎn)生,程序?qū)⒈罎ⅰ?/p>


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號