1. 程式人生 > >讀《Go並發編程實戰》第4章 流程控制方式

讀《Go並發編程實戰》第4章 流程控制方式

go並發編程實戰

說實話,該書前面講的枯燥冗長,看的有點打瞌睡,而我自己又是有一個有強迫癥的人,喜歡一個字一個字地摳,最終結果是一看就困,然後轉天再看再困,依次循環......。

這就總會讓我自己有點遐想,自己也寫一本關於Go的書算了,但因為平時真的太忙了,稍有時間時又貢獻給我女兒。我想後面我錄一些視頻,幫助那些想學習編程的人快速入門,打消非計算機專業人員的入門門檻。

好了,廢話有點多,還是耐著性子把作者郝林的這本書看完。


Go語言與其它編程語言類似,也有if語句、switch語句、for循環語句、goto語句,但不同之處在於Go語言的循環語句只有for,沒有平時我們見的while、do-while;在其它編程語言中我們經常聽到盡量避免使用goto語句,而讀過謝孟軍的beego代碼的人應該能看到謝大神使用了不少goto語句;此外,Go語言還有defer語句、針對異常處理引入了panic和recover語句等,本章就基本圍繞著這些展開。


4.1 基本流程控制

首先看一個源代碼:

package main

import (
    "fmt"
)

func main(){
    v := []int{1, 2, 3}
    if v != nil{
         var v int = 123
         fmt.Printf("%v\n", v)
    }
    fmt.Printf("%v\n", v)
}

第一次看Go代碼的人可能會有點不舒服,不過沒有關系,看的多了就舒服了,掌握編程語言最大的訣竅就是多寫,隨便解釋一下:

  • package和import不用多說,屬於工程化思想中的體系部分

  • v := []int{1,2,3},上看下看左看右看,也沒有看到v的定義呢?其實這裏:=就是定義加賦值

好,偏離原文太多了,原文在這裏主要說代碼塊和作用域。


4.1.1 代碼塊和作用域

所謂代碼塊就是一個由花括號“{”和“}”括起來的若幹表達式和語句的序列。當然,代碼塊中也可以不包含任何代碼 ,即為空代碼塊。就上面源代碼為例,代碼塊就是main()函數中的內容。

那麽這個程序運行結果是什麽呢?結果如下:

123

[1 2 3]

之所以兩次打印內容不同,就是由於作用域的原因,先采用作者的原話。

我們可以在某個代碼塊中對一個已經在包含它的外層代碼塊中聲明過的標識符進行重聲明。並且,當我們在內層代碼塊中使用這個標識符的時候,它代表的總是它在內層代碼塊中被重聲明時與它綁定在一起的那個程序實體。也就是說,在這種情況下,在外層代碼中聲明的那個同名標識符被屏蔽了。

看懂了嗎?是不是有點繞死了?腦子稍短路一下就死機了,用人話來說是這樣的:

main()函數是一個大的代碼塊,它裏面又包括了一個小的代碼塊(對應的if語句),這相當於一間大房子(main)裏面有一個小臥室(if{}),大房子客廳裏有一個叫郝林的人,小臥室中也有一個叫郝林的人,且房子隔音效果非常好。當你進入小臥室時輕輕地喊一聲“郝林”,那麽是小臥室的郝林會回應你,因為大房子客廳中的那個郝林聽不到;同樣地,當你進入大房子客廳時,你輕輕地喊一聲“郝林”,那麽是大房子客廳的郝林會回應你,因為小房子中的那個郝林聽不到。

如果您學過程序編譯原理的話,就很容易明白其中的原理,因為在編譯時,這根本就是兩個不同的變量,此處不再展開,所以有時候想想視頻有市場是應當的,如果是視頻兩句話就能交待清楚,但文字就得啰嗦很多。


if 100 < number{
     number++
}

這個是一個if語句,當然也符合代碼塊的定義,由花括號括起來的若幹表達式和語句的序列。

{
}

這個也是一個代碼塊,盡快沒有任何內容,由花括號括起來的若幹表達式和語句的序列,這裏的若幹包括0。

for i :=0; i <100; i++ {
    i = i + 4
}

同樣,這也是一個代碼塊。


作者又說,在3.3.3節講過,基本數據類型的值都無法與空值nil進行判等,而上面源代碼if v != nil { }就沒有編譯錯誤,是因為這裏的v代表的是一個切片類型而不是一個string類型值。這裏稍有點要講解的內容,一般語言是沒有切片的,這是一個特別的類型,回頭我做成一個演示動畫一看就很清楚明白。


4.1.2 if語句

Go語言的if語句總是以關鍵字if開始,在這之後,可以跟一條簡單語句(這裏可以的意思是說,也可以沒有),然後是一個作為條件判斷的布爾類型的表達式以及一個用花括號“{”和“}”括起來的代碼塊”。

上面這句話比較常,簡單地理解,就是if語句必須是這種形式“if +條件判斷 + {}”,舉兩個粟子:

var i int = 0

if i < 10 {
     i++
}

這個if語句就是典型的if+條件判斷+{ },其中條件判斷為i<10。當然這個if語句還可以這樣寫:

if i:=0; i <10 {
     i++
}

這個if語句就是上面藍色字體中說的,在if之後可以跟一條簡單語句i:=0,即變量i的定義和賦值語句。當然這個if語句還可以改寫為:

if i:=0; i <10; i++{

}

這裏把i++也放到了if和“{”之間。引用一下原書內容:

常用的簡單語句包括短變量聲明、賦值語句和表達式語句。除了特殊的內建函數和代碼包unsafe中的函數,針對其他函數和方法的調用表達式和針對通道類型值的接收表達式都可以出現在語句上下文中。換名話說,它們都可以稱為表達式語句。在必要時,我們還可以使用圓括號“(”和“)”將它們括起來。其他的簡單語句還包括發送語句、自增/自減語句和空語句”。

看懂沒有?若沒有看懂就算了,本書作者定義嚴謹,造成沒有接觸過的人很難理解 :)


下面引用作者的一個例子:

if 100 < number{
    number++
} else {
    number--
}

可能沒有接觸過的編程語言的小夥伴會問,怎麽還有else呀?這還算不算if語句,哥告訴你,這才是正宗的,別無二家,回到原書內容。“可能讀者已經註意到了,其中的條件表達式100<number並沒有被圓括號括起來。實際上,這也是Go語言的流程控制語句的特點之一。另外,跟在條件表達式和else關鍵之後的兩個代碼塊必須由“{”和“}”括起來。這一點是強制的,不論代碼包含幾條語句以及是否包含語句都是如此”。

這些東西沒有必要特別記憶,多寫幾個例子,自然而然您就知道什麽是正確的什麽是錯誤的。

上面示例中,有兩個特殊符號:++和--。它們分別代表了自增語句和自減語句。註意它們並不是操作符。++的作用是把它左邊的標識符代表的值與無類型常量1相加並將結果再賦給左邊的標識符,而--的作用則是把它左邊的標識符代表的值與無類型常量1相減並將結果再賦給左邊的標識符。也就是說,自增語句number++與賦值語句number=number+1具有相同的語義,而自減語句number--則與賦值語句number=number-1具有相同的語義。另外,在++和--左邊的並不僅限於標識符,還可以是任何可被賦值的表達式,比如應用在切片類型值或字典類型值之上的索引表達式"。


【更多慣用法】:

這有點類似英語常用對話300句。

func Open(name string) (file *File, err error)

這個就是常見的函數慣用用法,該函數來自標準庫中os包。

由於在Go語言中一個函數可以返回多個結果,因此我們常常會把函數執行的錯誤也作為返回結果之一,該函數表達意思是說,您指定一個文件路徑讓Go語言幫您把文件內容讀出來,為了讀出文件內容,該函數返回您文件的句柄(即第一個參數),同時也返回一個錯誤(即第二個參數),用以表達在打開文件時是否發生了錯誤。具體怎麽用呢?

f, err := os.Open(name)

if err != nil {
     return err
}

繞了半天後,還是回到if語句上。“總之,if語句常被用來常規錯誤”。

在通常情況下,我們應該先雲檢查變量err的值是否為nil,如果變量err的值不為nil,那麽就說明os.Open函數在被執行過程中發生了錯誤。這時的f變量的值肯定是不可用的。這已經是一個約定俗成的規則了”。

另外,if語句常被作為衛述語句。衛述語句是指被用來檢查關鍵的先決條件的合法性並在檢查未通過的情況下立即終止當前代碼塊的執行的語句。其實,在上一個示例中的if語句就是衛述語句中的一種。它在有錯誤發生的時候立即終止了當前代碼塊的執行並將錯誤返回給外層代碼塊”。

通過理解一下這段藍色的文字,通常我們寫程序是這樣的:

func update(id int, deptment string) bool {
     if id <=0 {
          return false
     }
     // 省略若幹行
     return true
}

這個沒毛病,update函數開始處的那個if語句就是衛述語句。該函數可以稍改造一下:

func update(id int, deptment string) error {
     if id <=0 {
         return errors.New("The id is INVALID!")
     }
     // 省略若幹行
     return nil
}

下面這個update返回結果不再是bool值,而是error值,它可以表示在函數執行期間是否發生了錯誤,而且還可以體現出錯誤的具體描述。


4.1.3 switch語句

switch語句與if語句類似,都是一種多分支執行語句,剛接觸編程的人可能有疑惑,為什麽要提供兩種呢?我是否只用一個就可以了?

當然,您只用其中之一就足夠了,為什麽要提供兩種呢?簡單理解還是慣用習慣吧。

§1. 組成和編寫方法

switch可以使用表達式或者類型說明符作為case判定方法。因此switch語句也就可以分為兩類:表達式switch語句和類型switch語句。在表達式switch語句中,每一個case攜帶的表達式都會與switch語句要判定的那個表達式(也稱為switch表達式)相比較。而在類型switch語句中,每個case所攜帶的不是表達式而是類型字面量,並且switch語句要判定的目標也變成了一個特殊的表達式。這個特殊表達式的結果是一個類型而不是一個類型值。

在女兒的哭聲中我讀這句話真的好吃力,靜下心來也不難理解,看例子就好:


§2. 表達式switch語句

switch 2*3+5{
    default:
         fmt.Println("運算錯誤!")
    case 5 + 5:
         fmt.Println("結果為10.")
    case 5 + 6:
         fmt.Println("結果為11.")
}

快看,快看,switch後面的2*3+5是一個表達式,第一個case後面的5+5也是一個表達式,第二個case後面的5+6也是一個表達式,所以這是一個典型的表達式switch語句。

在表達式switch語句中,switch表達式和case攜帶的表達式都會被求值。

程序運行時,先找計算第一個case後面的表達式得到10,然後與switch的表達式值11進行比較,發現10≠11,接著計算第二個case後面的表達式得到11,然後與switch的表達式值11進行比較,發現兩者相同,打印出“結果為11.”後就退出該代碼塊。

switch content {
    default:
        fmt.Println("Unknown Language.")
    case "Python":
        fmt.Println("An Interpreted Language.")
    case "Go":
        fmt.Println("A Compiled Language.")
}

這也是一個表達式switch語句,您可能會想這都沒有計算,怎麽是一個表達式呢?

姐,表達式不僅僅是數學運算,字符串也是喲,如果您實在感覺不順眼,改造一下:

switch content := getContent(); content {
    default:
        fmt.Println("Unknown Language.")
    case "Python":
        fmt.Println("An Interpreted Language.")
    case "Go":
        fmt.Println("A Compiled Language.")
}

在這個示例中,switch語句先調用getConent()函數,並且把它的結果賦給了新聲明的變量content,後面緊接著的就是對content的值進行判定。看著像是表達式switch嗎?


現在來看case語句。一條case語句由一個case表達式和一個語句列表組成,並且這兩者之間需要用冒號“:”分隔,在上例的switch語句中,一共有3個case語句,註意default case是一種特殊的case語句。

一個case表達式由一個case關鍵字和一個表達式列表組成。註意,這裏說的是一個表達式列表,而不是一個表達式。這意味著,一個case表達式中可以包含多個表達式。現在,我們利用這一特性來改造一下上面的switch語句:

switch content := getContent(); content {
    default:
        fmt.Println("Unknown Language.")
    case "Python", "Ruby":
        fmt.Println("An Interpreted Language.")
    case "Go", "Java", "C":
        fmt.Println("A Compiled Language.")
}

其中"Python"和"Ruby"形成一個表達式列表放到了case後面,同理"Go"、"Java"和"C"也形成一個表達式列表放到了另一個case後面。


由於Go語言有一個fallthrough關鍵字,所以上面示例可改造如下:

switch content := getContent(); content {
    default:
        fmt.Println("Unknown Language.")
    case "Python":
        fallthrough
    case "Ruby":
        fmt.Println("An Interpreted Language.")
    case "Go", "Java", "C":
        fmt.Println("A Compiled Language.")
}

當content內容為"Python"時,盡管會匹配"Python"對應的case語句,但由於fallthrough關鍵字的存在,它讓程序穿越它而向下執行,所以會打印“An Interpreted Language.”,但一定要記住的是fallthrough只能穿越一次。


§3. 類型switch語句

先看個示例吧:

switch v.(type){
    case string:
        fmt.Printf("The string is ‘%s‘.\n", v.(string))
    case int, uint, int8, uint8, int16, uint16, int32, uint32, int64, uint64:
        fmt.Printf("The integer is %d.\n", v)
    default:
        fmt.Printf("Unsupported value. (type=%T)\n", v)
}

仔細看case後面的表達式,都是string, int, int8, int16等Go語言的類型,所以所謂類型switch語句就是對類型進行判定,而不是對值進行判定,其他方面與表達式switch一般無二。


把這個代碼跑通,需要補充點關於v的內容:

var v interface{} = "aaabbb"

註意這裏是把v定義為接口,而非string,即var v string = "aaabbb",如果真的這樣定義了v,那麽Go的編譯器就會拋個異常給你看,並說:“我靠,你都知道是什麽類型了,還讓switch判斷,這是耍我玩呀!”。


現在我們來具體分析這段示例代碼,這個switch語句共包含了3條case語句。

> 第一條case語句的表達式包含了類型string字面量,這意味著如果v的類型是string類型,那麽該分支就會被執行。在這個分支中,我們使用類型斷言表達式v.(string)把v的值轉換成了string類型的值,並以特定格式打印出來;

> 第二條case語句中的類型字面量有多個,包括了所有的整數類型,這就意味著只要v的類型屬於整數類型,該分支就會被執行。在這個分支中,我們並沒有使用類型斷言表達式把v的值轉換成任何一個整數類型的值,而是利用fmt.Printf函數直接打印出了v所表示的整數值;

> 如果v的類型既不是string類型也不是整數類型,那麽default case的分支將會被執行,並使用標準輸出打印v的動態類型(%T)。


需要特別註意的是:fallthrough語句不允許出現在類型switch語句中的任何case語句的語句列表中


最後,值得特別提出的是,類型switch語句的switch表達式還有一種變形寫法。

var v interface{} = "aaabbb"
switch i := v.(type) {
    case string:
        fmt.Printf("The string is ‘%s‘.\n", i)
    case int, uint, int8, uint8, int16, uint16, int32, uint32, int64, uint64:
        fmt.Printf("The integer is %d.\n", i)
    default:
        fmt.Printf("Unsupport value. (type=%T)\n", i)
}

請註意switch表達式位置上的i := v.(type),這實際上是一個短變量聲明,當存在這這種形式的switch表達式的時候,就相當於這個變量i被聲明在了每個case語名的語句列表的開始處。在每個case語句中,變量i的類型都是不同的,它的類型會和與它處於同一個case語句的case表達式包含的類型字面量所代表的那個類型相等。例如,上面的示例中第一個case語句相當於:

case string:

i := v.(string)

fmt.Printf("The string is ‘%s‘.\n", i)


是不是再次被作者的富有九曲十折的表達方式折服?其實作者想表達的意思,簡單來說是這樣的:

switch v.(type){

case string:

fmt.Printf("The string is ‘%s‘.\n", v.(string))

}

如果switch表達式只是取變量v的類型,那麽在case語句中必須把變量v進行強制類型轉換;


switch i := v.(type){

case string:

fmt.Printf("The string is ‘%s‘.\n", i)

}

如果switch表達式中有對變量v的類型賦值給i,那麽當進入某個case語句中時,相當於變量i在每個case語句中都有一次具體的類型轉換,switch那麽可以把它理解為模板。


好吧,如果越說越胡塗,請您暫時記住這兩種用法就好,隨著代碼寫的越來越多,就會逐步明白的。


§4. 更多慣用法

好了,又到了常用英語300句了 :)

在不少情況下switch表達式是缺省掉的,即:

switch{
     case number < 100:
          number++
     case number < 200:
          number--
     default:
          number -= 2
}

看這裏的switch表達式消失了,此種情況下該switch語句的判定目標被視為布爾值true,也就是說,所有case表達式的結果值都應該是布爾類型,所以才有switch代碼塊中每個case語句都是number在與數值進行比較,以獲得布爾值。

本文出自 “青客” 博客,轉載請與作者聯系!

讀《Go並發編程實戰》第4章 流程控制方式