Golang怪談

本文可以隨意轉載,轉載請標明作者和來源。

引子

本文題目凸顯一個‘怪’字,怪即為奇異,Go語言很多特點和特性可稱之為奇異,掌握了這些奇異之處,我們自然也就了解了Go語言的精髓。

概述

Go語言作為一門新時代的編譯語言,以排山倒海之勢迅速占領后臺服務開發陣地,在C++標準委員會急不可耐的把原本就極其復雜的C++語言用C++11變的更加復雜以后,作為面向過程、面向對象、泛型編程語言的C++逐漸被追求最新技術的程序員所拋棄,而Go語言以其無比簡單的語法,極其高效的運行效率獲得了越來越多人的親睞,Go語言原生支持并發的特性使其在現今的并行計算時代更具有非凡意義。

本文主旨

本文主要通過簡單的幾個例子的介紹,為讀者構建一個Go語言的基本印象,本文不是Go語言教程,作者在這里僅用調侃的態度來給沒有接觸過Go語言的讀者提供一些Go語言特性的信息,希望在看到這些特性后,我能欣喜的看到有人可以捧起書真正進入Go語言的世界。

第一怪:老朽偽裝小鮮肉

這么是說誰呢,其實呢,我去搜索Go語言介紹的時候,驚人的發現一個萌萌的年輕人坐在那里,然后我就以為這樣一個小鮮肉竟然發明了Go語言,真是“江山代有才人出,后浪死在沙灘上”。但是別急,再繼續搜索后我發現,Go語言的創造者羅布·派克(Rob Pike)其實早就名聲斐然,出生于1965年的他并不年輕,他可是UTF-8設計者,同時羅布·派克跟Unix淵源極深,他和Ken Thompson以及 Dennis M.Ritche一起開發了Unix操作系統,作為資深Geek,羅布·派克不忘在體育界刷存在感,他在1980年奧運會上轉了一圈,拿了個射箭銀牌。閑著沒事他還在天文學那邊兒插一腳,真是程序員大師中的一朵奇葩。

萌rob.png

順便說一句,由于Go語言在Google大行其勢,原來的香餑餑Python之父吉多·范羅蘇姆(Guido Van Rossum) 于2012年底黯然離開谷歌加入到了Dropbox,這也暗示著python時代將要終結,Go語言的時代正在到來。

老rob.png

第二怪:強迫癥晚期誰來救

我們正式進入到Go語言的世界,看看Go語言的一些特性,首先以一個例子開頭:

package main
import(
    "os"http://@1:先import進來,一會兒就發力
    "fmt"
)
func main()
{//@2:還是換個行吧,這樣看起來帥帥的
  x := 1//@3:我先占個座,一會來自習
  fmt.Println("我是第一個go程序")
}

這是我們的第一個例子,對于熟悉其他語言的人來說,這看起來是一個最正常不過的例子,但是其實這個例子中有三處語法錯誤,你們先找茬,我先簡單介紹一下Go語言的基本語法,文件開頭一般要命名一個包名(package),相同包名即是一家子,如果有package main即為主執行程序,通過go install命令即可進行安裝,生成可執行二進制,第二行的import說明需要import的包,如果只有一行可以使用類似import "fmt"這種語法,這個程序的例子多個import括號方式會比較簡潔,Go語言的函數是以func開始,局部變量使用:=運算符時,編譯器可以自動推導出變量的類型。說了這么多,我們該把語法錯誤指出來了,@1這一處錯誤是因為,在程序文件中,根本沒有使用os包的位置,由于其多余,Go語言強行定義其為語法錯誤;@2這一處更加體現了羅布·派克強迫癥晚期患者的癥狀,他要求如第7行的大括號必須緊跟在main()的后面,否則就是語法錯誤;@3和@1類似,這是變量級別的使用要求,不允許任何變量定義未使用。

羅布·派克定義了大量的規則用來保證程序員少犯錯,這大概是其見過太多C和C++過于自由的導致其難用的最痛的領悟吧。

第三怪:匿名字段真不賴

直接上例子:

package main
import "fmt"

type Human struct{
  name string
  age int
  weight int
}
type Student struct{
  Human //@1
  speciality string
}

func main(){
  mark := Student{Human{"Mark", 25, 120}, "e-commerce"}
  fmt.Println(mark.name, mark.age, mark.weight, mark.speciality)//@2
}

這個例子講的是Go語言中的struct,Go語言的struct和C++的class比較相似,但是你們可能會奇怪第@1行的Human是要鬧哪樣,你的對象呢,你的對象呢?其實這里就到Go語言中的匿名字段的神奇之處了。在講匿名字段之前,需要解釋個事情,Go語言變量命名方式是絕無僅有的奇葩,聰明的你應該已經發現了,這里面變量都是在類型前面,導致如果你給變量附初值時會出現var i int=8這種看起來很欠揍的語法,不過用習慣也就沒什么了,語言本身就是一堆規則,大師制定規則,碼農按照規則拉磨。

我們言歸正傳,繼續講struct,匿名字段其實相當于在Student使用Human類型時,默認編譯器將Human自動展開,我們看第@2行即可以發現Human.name直接過繼給了Student,匿名字段就是可以把兒子變孫子,這種奇葩的事情羅布.派克不是首創,咱們中國老祖宗唐德宗就有認自己的孫子為兒子的事兒,這里面有啥八卦隱情這里不表,估計羅布.派克也不知道這事兒。有的人可能一定要問了,如果倆孫子重名可咋辦,萬一孫子跟兒子重名不也亂套了么,這個你不用著急,如果發生這種情況,編譯器還是會及時發現,星星還是那顆星星,兒子還是那個兒子,具體可以做實驗去體驗吧。

第四怪:interface你陷害

Go語言的interface可以說是面向對象設計中極其巧妙的實現,其隱含接口實現不但使得代碼簡潔,同時內容組織無與倫比的方便。繼續讀代碼說話:

package main
import(
  "fmt"
  "math"
)
type geometry interface {
  area() float64
  perim() float64
}
type square struct{
  width, height float64
}
func (s square) area() float64{
  return s.width * s.height
}
func (s square) perim() float64{
  return 2*s.width + 2*s.height
}
type circle struct{
  radius float64
}
func (c circle) area() float64{
  return math.Pi * c.radius * c.radius
}
func (c circle) perim() float64{
  return 2*math.Pi*c.radius
}
func measure(g geometry){
  fmt.Println(g)
  fmt.Println(g.area())
  fmt.Println(g.perim())
}
func main(){
  c := circle{radius:3}
  s := square{width:4.0, height:5.0}
  measure(c)
  measure(s)
}

這個例子略長,前面介紹的struct我們現在已經熟悉了,重點看一下interface的定義和實現。我們可以看到一個geometry interface的定義,這個接口里定義了areaperim兩個方法。重點看一下成員函數的實現方法,Go語言成員函數與struct也是松耦合的,這里我們要重點關注的是括號,以squarearea方法為例,我先看第一個括號,我們把類型square s稱為這個方法接收者,其實就是C++的成員函數的不同表征方法,在python中,一般是用傳入self來實現的。第二括號才是方法自己的括號,這里我們沒有帶參數,最后一個float64是返回值類型,如果有多個返回值需要用括號括起來。那么問題來了,一個成員方法你最多可以看到多少括號,其實可能不止三個,但是會有三部分。

到此有人可能會問了,interface你陷害,你瞎掰吧,說了這么多,還不說陷害的事兒。我們言歸正傳,為什么說interface陷害呢,因為一個struct的方法只要實現了interface的方法組合,那么Go語言就認為我們實現了這個接口,這中關聯是隱式的,也就是說,你寫了一個struct,實現了一堆方法,可能你就順便實現了另外一堆接口,這些接口可能連你都不知道。我們看measure方法定義,其接受的參數是一個geometry接口,可以直接傳入circlesquare對象,因為這兩個struct都實現了areaperim方法,那么他們就實現了geometry接口,這兩個struct啥都沒說,就被陷害說他們實現了geometry接口。
實際上在fmt.Println接收的參數就是不定長interface參數Stringer,其定義為:type Stringer interface { String() string },由此我們可以看出,只要你的struct實現了String方法,那么你就實現了Stringer接口,也就是說,這樣你就可以打印這個struct對象了,這有點兒類似Java中的toString,但是實現的優雅程度就是云泥之別了。順便說一句,所有的類型都實現了空接口interface{},也就是說,如果一個函數參數為空接口interface{}類型,那么這個函數可以接受任何參數,這有點兒像C語言的void,但是Go語言要更加強大。

interface的設計是Go語言的神來之筆,使用過程中你會越來越體會到interface之精妙。

第五怪:加鎖啥的都狗帶

通過通信來共享內存,而非通過共享內存來通信,Go語言原生支持并發,并通過goroutinechannel將并發編程的簡潔性和高效性體現的淋漓盡致,我們再也不用擔心加鎖問題了,在程序員上空死鎖的陰云散去(其實還是會寫出死鎖的程序,具體可以查找相關聊),抬頭再看,并發編程一片晴空。我們以生產者消費者問題舉例,我們會發現gorutine實現并發是怎樣一種優雅:

package main
import "fmt"
import "time"
func producer(id int, item chan int) {
    for i := 0; i < 10; i++ {
        item <- i
        fmt.Printf("producer %d produces data: %d\n", id, i)
        time.Sleep(1*time.Second)
    }
}
func consumer(id int, item chan int) {
    for i := 0; i < 20; i++ {
        c_item := <-item
        fmt.Printf("consumer %d get data: %d\n", id, c_item)
        time.Sleep(1*time.Second)
    }
}
func main() {
    item := make(chan int, 6)//@1
    go producer(1, item)
    go producer(2, item)
    go consumer(1, item)
    time.Sleep(30 * time.Second)//等待其他goroutine都執行完退出
}

我們看@1行,我們通過make語法建立了一個緩沖為6的int管道,生產者只需要關心生產,把生產好的數據直接扔進管道,消費者呢,不用關心生產者的任何細節,只需要從管道里取數據,生產端和消費端都是阻塞的,當管道為空時,消費端阻塞,當管道滿時,生產端阻塞。關鍵字go定義我們新開了一個goroutine,每個goroutine你可以理解成線程,但是goroutine更加輕量和高效,一個程序起成千上萬個goroutine毫無壓力。
我們剛剛看到有緩沖channel類似于消息隊列的神勇表現,接下來我們看看無緩沖channel在多個goroutine同步的精彩演繹:

package main
import "fmt"
func fibonacci(c, quit chan int){
    x,y := 1,1
    for{
        select{
            case c<-x:
                x,y = y,x+y
            case <-quit:
                fmt.Println("quit")
                return
        }
    }
}
func main(){
    c := make(chan int)
    quit := make(chan int)
    go func(){
        for i:=0; i<10; i++{
            fmt.Println(<-c)
        }
        quit <-0
    }()
    fibonacci(c, quit)
}

我們這個程序用了一個select關鍵字,沒錯,這個select的作用跟網絡通信模型的select非常相似,select監聽channel中的數據流,默認select是阻塞的,當管道中有發送或者接收行為時,select才會執行,當有多個管道都準備好時,select會從中隨機取一個執行,這個程序我們不需要用time.Sleep等待其他goroutine退出,在quit管道被寫入0之后,select偵測到,執行case <-quit后程序退出。

Go語言并發編程有效利用多核CPU,把比thread更加輕量、高效的goroutine與管道相結合,極度優雅的實現數據共享和同步,加鎖什么的確實可以狗帶了。

第六怪:靜態編譯沒依賴

動態鏈接庫在計算機的蠻荒年代起了非常大的作用,那時的內存還是論K的,硬盤是論M的,動態鏈接庫可以在不同進程間通過共享代碼來節省內存,使用動態鏈接庫編譯出的二進制也非常小,這就使得磁盤空間使用和拷貝代價很低。但是動態鏈接庫的缺點也是顯而易見的,甚至因為動態鏈接庫過于混亂產生了專門的名詞,相關性依賴地獄(dependence hell),不同程序之間的依賴讓無數程序員徹夜調試,而動態鏈接庫給我們帶來的好處在今天看來實在是不值得的,它為我們節省的內存和磁盤空間實在微不足道,動態鏈接沖突導致的問題跟帶來的好處比起來就太大了,可以說,現在如果還抱著動態鏈接庫不放就是丟西瓜撿芝麻完全是得不償失。

Go語言作者羅布·派克顯然也看到了這一點,他的解決方案非常簡單,二進制不依賴任何動態鏈接庫,所有的編譯都是靜態鏈接,我們再也不用擔心換了一臺機器運行程序無法執行的問題了,這種做法只是損失了很少的內存和磁盤空間,但是帶來整體性(integrity)的極大好處。

第七怪:編碼運行真是快

有人把Go語言稱為21世紀的C語言,跟C語言相比,Go語言的運行效率當之無愧,但是如果把C語言的編碼效率與Go語言相比,C語言會被甩出好幾條街,可以這么說,Go語言是python和C的合體,它兼顧了C語言的運行效率和python的編碼效率。Go語言的關鍵字只有25個,Go語言的所有循環的寫法只有一個for把其他語言的while、foreach等一堆亂七八糟的命名整合成一個,使用極其簡潔。其運行效率到底有多高呢,下圖是benchmarksgame網站上對比Go語言和C執行不同算法執行效率,除了個別算法運行效率差別較大,大部分算法Go語言執行效率跟C相差可以忽略不計。

2016-06-08 12_45_09-Go vs C gcc (64-bit Ubuntu quad core) _ Computer Language Benchmarks Game.png

我們這種對比雖然可能不一定公平,我們可以直觀上感受Go語言運行的高效。

第八怪:異常處理沒有try

“作為現代編程語言一枚,try-catch都沒有,你還想讓我在編程界混么”,Go語言的內心OS一定是這樣。現實是羅布·派克根本不想讓try-catch出現,原因我們來看一段python代碼:

def main():
    try:
        check_filename()
        check_filesize()
        check_filelines()
        read_file()
    except:
        exit(1)
#endf main


if __name__ == "__init__":
    main()

可能有人會說,這代碼挺正常啊,出問題就退出唄,這么寫代碼的人真是被python給慣的太懶了,多少行代碼都能用一個try-catch給包起來,根本不仔細考量到底可能會發生哪些異常,這些所謂的異常可能根本不是異常,曾經見過在python有人用try-catch代替if-else來對類似于字段個數判斷處理,這種程序執行結果是沒有錯,但是無論是執行效率還是代碼可讀性以及代碼可維護性上都會有問題。羅布·派克強制讓編碼人員仔細考慮邏輯,對于可以預期的異常就不應該用try-catch來處理,直接就是代碼處理邏輯分支,而不可預期的異常就應該拋出來,所以在Go語言中遇到數組越界程序就會直接painc程序退出,另外Go語言還有個recover機制,通過調用recover捕獲到panic的輸入值,恢復正常的執行。

沒有try-catch機制實際上是對程序員的嚴格要求,這也是對程序的一種保護。panicrecover在使用時都應該慎之又慎,作為程序員的我們最重要的是嚴謹的考慮代碼邏輯,盡量避免panic

回顧

本文從8個不同角度介紹了Go語言的由來和設計,它們分別是:

第一怪:老朽偽裝小鮮肉
第二怪:強迫癥晚期誰來救
第三怪:匿名字段真不賴
第四怪:interface你陷害
第五怪:加鎖啥的都狗帶
第六怪:靜態編譯沒依賴
第七怪:編碼運行真是快
第八怪:異常處理沒有try

總結

Go語言作者羅布·派克作為貝爾實驗室Unix先驅,看慣了各種編程語言刀光劍影、鼓角爭鳴,各種語言你方唱罷我登場,它們雖然在一定程度上解決了之前使用語言的一些弊端,如C++語言把面向對象引進,Java把設計模式發揚光大,python使寫代碼接近自然語言,但是他們的時代畢竟要過去,Go語言簡潔的語法,原生的并發支持,interface是精妙設計無一不顯示了作者對編程語言的全新的設計和理解,羅布·派克把我們帶入了一個新的編程語言世界,在這里,我們用簡單嚴謹的語法書寫程序藝術,而Go語言以其高效運行來回報我們,生活在Go語言的世界里是程序員的幸福,用時下流行的話來說,用Go語言編程的人運氣都不會太壞。

注:本文大部分代碼來自謝孟軍的《Go web編程》一書。

參考文獻:

  1. Go web 編程
  2. 維基百科
  3. 酷殼-程序語言性能比拼
  4. golang并發編程之生產者消費者模式
    ~
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 官方網站:https://golang.org/標準庫文檔:https://golang.org/pkg/在線編碼...
    技術學習閱讀 2,329評論 2 39
  • 能力模型 選擇題 [primary] 下面屬于關鍵字的是()A. funcB. defC. structD. cl...
    _張曉龍_閱讀 24,867評論 14 224
  • 控制并發有三種種經典的方式,一種是通過channel通知實現并發控制 一種是WaitGroup,另外一種就是Con...
    wiseAaron閱讀 10,686評論 4 34
  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,837評論 18 139
  • 今天朋友問我上海有什么好的,是呀,上海有什么好,以至于很多人來到這個城市,有的甚至愛上這座城市。坦白講,一開始來到...
    給我一只猴子閱讀 325評論 0 1