天天看點

Go語言核心36講(Go語言進階技術十二)--學習筆記

現在,讓我們暫時走下神壇,回歸民間。我今天要講的if語句、for語句和switch語句都屬于 Go 語言的基本流程控制語句。它們的文法看起來很樸素,但實際上也會有一些使用技巧和注意事項。我在本篇文章中會以一系列面試題為線索,為你講述它們的用法。

那麼,今天的問題是:使用攜帶range子句的for語句時需要注意哪些細節? 這是一個比較籠統的問題。我還是通過程式設計題來講解吧。

本問題中的代碼都被放在了指令源碼檔案 demo41.go 的main函數中的。為了專注問題本身,本篇文章中展示的程式設計題會省略掉一部分代碼包聲明語句、代碼包導入語句和main函數本身的聲明部分。

我先聲明了一個元素類型為int的切片類型的變量numbers1,在該切片中有 6 個元素值,分别是從1到6的整數。我用一條攜帶range子句的for語句去疊代numbers1變量中的所有元素值。

在這條for語句中,隻有一個疊代變量i。我在每次疊代時,都會先去判斷i的值是否等于3,如果結果為true,那麼就讓numbers1的第i個元素值與i本身做按位或的操作,再把操作結果作為numbers1的新的第i個元素值。最後我會列印出numbers1的值。

是以具體的問題就是,這段代碼執行後會列印出什麼内容?

這裡的典型回答是:列印的内容會是[1 2 3 7 5 6]。

你心算得到的答案是這樣嗎?讓我們一起來複現一下這個計算過程。

當for語句被執行的時候,在range關鍵字右邊的numbers1會先被求值。

這個位置上的代碼被稱為range表達式。range表達式的結果值可以是數組、數組的指針、切片、字元串、字典或者允許接收操作的通道中的某一個,并且結果值隻能有一個。

對于不同種類的range表達式結果值,for語句的疊代變量的數量可以有所不同。

就拿我們這裡的numbers1來說,它是一個切片,那麼疊代變量就可以有兩個,右邊的疊代變量代表當次疊代對應的某一個元素值,而左邊的疊代變量則代表該元素值在切片中的索引值。

那麼,如果像本題代碼中的for語句那樣,隻有一個疊代變量的情況意味着什麼呢?這意味着,該疊代變量隻會代表當次疊代對應的元素值的索引值。

更寬泛地講,當隻有一個疊代變量的時候,數組、數組的指針、切片和字元串的元素值都是無處安放的,我們隻能拿到按照從小到大順序給出的一個個索引值。

是以,這裡的疊代變量i的值會依次是從0到5的整數。當i的值等于3的時候,與之對應的是切片中的第 4 個元素值4。對4和3進行按位或操作得到的結果是7。這就是答案中的第 4 個整數是7的原因了。

現在,我稍稍修改一下上面的代碼。我們再來估算一下列印内容。

注意,我把疊代的對象換成了numbers2。numbers2中的元素值同樣是從1到6的 6 個整數,并且元素類型同樣是int,但它是一個數組而不是一個切片。

在for語句中,我總是會對緊挨在當次疊代對應的元素後邊的那個元素,進行重新指派,新的值會是這兩個元素的值之和。當疊代到最後一個元素時,我會把此range表達式結果值中的第一個元素值,替換為它的原值與最後一個元素值的和,最後,我會列印出numbers2的值。

對于這段代碼,我的問題依舊是:列印的内容會是什麼?你可以先思考一下。

好了,我要公布答案了。列印的内容會是[7 3 5 7 9 11]。我先來重制一下計算過程。當for語句被執行的時候,在range關鍵字右邊的numbers2會先被求值。

這裡需要注意兩點:

1、range表達式隻會在for語句開始執行時被求值一次,無論後邊會有多少次疊代;

2、range表達式的求值結果會被複制,也就是說,被疊代的對象是range表達式結果值的副本而不是原值。

基于這兩個規則,我們接着往下看。在第一次疊代時,我改變的是numbers2的第二個元素的值,新值為3,也就是1和2之和。

但是,被疊代的對象的第二個元素卻沒有任何改變,畢竟它與numbers2已經是毫不相關的兩個數組了。是以,在第二次疊代時,我會把numbers2的第三個元素的值修改為5,即被疊代對象的第二個元素值2和第三個元素值3的和。

以此類推,之後的numbers2的元素值依次會是7、9和11。當疊代到最後一個元素時,我會把numbers2的第一個元素的值修改為1和6之和。

好了,現在該你操刀了。你需要把numbers2的值由一個數組改成一個切片,其中的元素值都不要變。為了避免混淆,你還要把這個切片值賦給變量numbers3,并且把後邊代碼中所有的numbers2都改為numbers3。

問題是不變的,執行這段修改版的代碼後列印的内容會是什麼呢?如果你實在估算不出來,可以先實際執行一下,然後再嘗試解釋看到的答案。提示一下,切片與數組是不同的,前者是引用類型的,而後者是值類型的。

我們可以先接着讨論後邊的内容,但是我強烈建議你一定要回來,再看看我留給你的這個問題,認真地思考和計算一下。

先來看一段代碼。

我先聲明了一個數組類型的變量value1,該變量的元素類型是int8。在後邊的switch語句中,被夾在switch關鍵字和左花括号{之間的是1 + 3,這個位置上的代碼被稱為switch表達式。這個switch語句還包含了三個case子句,而每個case子句又各包含了一個case表達式和一條列印語句。

所謂的case表達式一般由case關鍵字和一個表達式清單組成,表達式清單中的多個表達式之間需要有英文逗号,分割,比如,上面代碼中的case value1[0], value1[1]就是一個case表達式,其中的兩個子表達式都是由索引表達式表示的。

另外的兩個case表達式分别是case value1[2], value1[3]和case value1[4], value1[5], value1[6]。

此外,在這裡的每個case子句中的那些列印語句,會分别列印出不同的内容,這些内容用于表示case子句被選中的原因,比如,列印内容0 or 1表示目前case子句被選中是因為switch表達式的結果值等于0或1中的某一個。另外兩條列印語句會分别列印出2 or 3和4 or 5 or 6。

現在問題來了,擁有這樣三個case表達式的switch語句可以成功通過編譯嗎?如果不可以,原因是什麼?如果可以,那麼該switch語句被執行後會列印出什麼内容。

我剛才說過,隻要switch表達式的結果值與某個case表達式中的任意一個子表達式的結果值相等,該case表達式所屬的case子句就會被選中。

并且,一旦某個case子句被選中,其中的附帶在case表達式後邊的那些語句就會被執行。與此同時,其他的所有case子句都會被忽略。

當然了,如果被選中的case子句附帶的語句清單中包含了fallthrough語句,那麼緊挨在它下邊的那個case子句附帶的語句也會被執行。

正因為存在上述判斷相等的操作(以下簡稱判等操作),switch語句對switch表達式的結果類型,以及各個case表達式中子表達式的結果類型都是有要求的。畢竟,在 Go 語言中,隻有類型相同的值之間才有可能被允許進行判等操作。

如果switch表達式的結果值是無類型的常量,比如1 + 3的求值結果就是無類型的常量4,那麼這個常量會被自動地轉換為此種常量的預設類型的值,比如整數4的預設類型是int,又比如浮點數3.14的預設類型是float64。

是以,由于上述代碼中的switch表達式的結果類型是int,而那些case表達式中子表達式的結果類型卻是int8,它們的類型并不相同,是以這條switch語句是無法通過編譯的。

再來看一段很類似的代碼:

其中的變量value2與value1的值是完全相同的。但不同的是,我把switch表達式換成了value2[4],并把下邊那三個case表達式分别換為了case 0, 1、case 2, 3和case 4, 5, 6。

如此一來,switch表達式的結果值是int8類型的,而那些case表達式中子表達式的結果值卻是無類型的常量了。這與之前的情況恰恰相反。那麼,這樣的switch語句可以通過編譯嗎?

答案是肯定的。因為,如果case表達式中子表達式的結果值是無類型的常量,那麼它的類型會被自動地轉換為switch表達式的結果類型,又由于上述那幾個整數都可以被轉換為int8類型的值,是以對這些表達式的結果值進行判等操作是沒有問題的。

當然了,如果這裡說的自動轉換沒能成功,那麼switch語句照樣通不過編譯。

Go語言核心36講(Go語言進階技術十二)--學習筆記

通過上面這兩道題,你應該可以搞清楚switch表達式和case表達式之間的聯系了。由于需要進行判等操作,是以前者和後者中的子表達式的結果類型需要相同。

switch語句會進行有限的類型轉換,但肯定不能保證這種轉換可以統一它們的類型。還要注意,如果這些表達式的結果類型有某個接口類型,那麼一定要小心檢查它們的動态值是否都具有可比性(或者說是否允許判等操作)。

因為,如果答案是否定的,雖然不會造成編譯錯誤,但是後果會更加嚴重:引發 panic(也就是運作時恐慌)。

我在上一個問題的闡述中還重點表達了一點,不知你注意到了沒有,那就是:switch語句在case子句的選擇上是具有唯一性的。

正因為如此,switch語句不允許case表達式中的子表達式結果值存在相等的情況,不論這些結果值相等的子表達式,是否存在于不同的case表達式中,都會是這樣的結果。具體請看這段代碼:

變量value3的值同value1,依然是由從0到6的 7 個整數組成的數組,元素類型是int8。switch表達式是value3[4],三個case表達式分别是case 0, 1, 2、case 2, 3, 4和case 4, 5, 6。

由于在這三個case表達式中存在結果值相等的子表達式,是以這個switch語句無法通過編譯。不過,好在這個限制本身還有個限制,那就是隻針對結果值為常量的子表達式。

比如,子表達式1+1和2不能同時出現,1+3和4也不能同時出現。有了這個限制的限制,我們就可以想辦法繞過這個對子表達式的限制了。再看一段代碼:

變量名換成了value5,但這不是重點。重點是,我把case表達式中的常量都換成了諸如value5[0]這樣的索引表達式。

雖然第一個case表達式和第二個case表達式都包含了value5[2],并且第二個case表達式和第三個case表達式都包含了value5[4],但這已經不是問題了。這條switch語句可以成功通過編譯。

不過,這種繞過方式對用于類型判斷的switch語句(以下簡稱為類型switch語句)就無效了。因為類型switch語句中的case表達式的子表達式,都必須直接由類型字面量表示,而無法通過間接的方式表示。代碼如下:

變量value6的值是空接口類型的。該值包裝了一個byte類型的值127。我在後面使用類型switch語句來判斷value6的實際類型,并列印相應的内容。

這裡有兩個普通的case子句,還有一個default case子句。前者的case表達式分别是case uint8, uint16和case byte。你還記得嗎?byte類型是uint8類型的别名類型。

是以,它們兩個本質上是同一個類型,隻是類型名稱不同罷了。在這種情況下,這個類型switch語句是無法通過編譯的,因為子表達式byte和uint8重複了。好了,以上說的就是case表達式的限制以及繞過方式,你學會了嗎。

我們今天主要讨論了for語句和switch語句,不過我并沒有說明那些文法規則,因為它們太簡單了。我們需要多加注意的往往是那些隐藏在 Go 語言規範和最佳實踐裡的細節。

這些細節其實就是我們很多技術初學者所謂的“坑”。比如,我在講for語句的時候交代了攜帶range子句時隻有一個疊代變量意味着什麼。你必須知道在疊代數組或切片時隻有一個疊代變量的話是無法疊代出其中的元素值的,否則你的程式可能就不會像你預期的那樣運作了。

​​https://github.com/MingsonZheng/go-core-demo​​

​​

Go語言核心36講(Go語言進階技術十二)--學習筆記