本文可以隨意轉(zhuǎn)載,轉(zhuǎn)載請(qǐng)標(biāo)明作者和來(lái)源。
引子
本文題目凸顯一個(gè)‘怪’字,怪即為奇異,Go語(yǔ)言很多特點(diǎn)和特性可稱之為奇異,掌握了這些奇異之處,我們自然也就了解了Go語(yǔ)言的精髓。
概述
Go語(yǔ)言作為一門新時(shí)代的編譯語(yǔ)言,以排山倒海之勢(shì)迅速占領(lǐng)后臺(tái)服務(wù)開發(fā)陣地,在C++標(biāo)準(zhǔn)委員會(huì)急不可耐的把原本就極其復(fù)雜的C++語(yǔ)言用C++11變的更加復(fù)雜以后,作為面向過程、面向?qū)ο?、泛型編程語(yǔ)言的C++逐漸被追求最新技術(shù)的程序員所拋棄,而Go語(yǔ)言以其無(wú)比簡(jiǎn)單的語(yǔ)法,極其高效的運(yùn)行效率獲得了越來(lái)越多人的親睞,Go語(yǔ)言原生支持并發(fā)的特性使其在現(xiàn)今的并行計(jì)算時(shí)代更具有非凡意義。
本文主旨
本文主要通過簡(jiǎn)單的幾個(gè)例子的介紹,為讀者構(gòu)建一個(gè)Go語(yǔ)言的基本印象,本文不是Go語(yǔ)言教程,作者在這里僅用調(diào)侃的態(tài)度來(lái)給沒有接觸過Go語(yǔ)言的讀者提供一些Go語(yǔ)言特性的信息,希望在看到這些特性后,我能欣喜的看到有人可以捧起書真正進(jìn)入Go語(yǔ)言的世界。
第一怪:老朽偽裝小鮮肉
這么是說(shuō)誰(shuí)呢,其實(shí)呢,我去搜索Go語(yǔ)言介紹的時(shí)候,驚人的發(fā)現(xiàn)一個(gè)萌萌的年輕人坐在那里,然后我就以為這樣一個(gè)小鮮肉竟然發(fā)明了Go語(yǔ)言,真是“江山代有才人出,后浪死在沙灘上”。但是別急,再繼續(xù)搜索后我發(fā)現(xiàn),Go語(yǔ)言的創(chuàng)造者羅布·派克(Rob Pike)其實(shí)早就名聲斐然,出生于1965年的他并不年輕,他可是UTF-8設(shè)計(jì)者,同時(shí)羅布·派克跟Unix淵源極深,他和Ken Thompson以及 Dennis M.Ritche一起開發(fā)了Unix操作系統(tǒng),作為資深Geek,羅布·派克不忘在體育界刷存在感,他在1980年奧運(yùn)會(huì)上轉(zhuǎn)了一圈,拿了個(gè)射箭銀牌。閑著沒事他還在天文學(xué)那邊兒插一腳,真是程序員大師中的一朵奇葩。

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

第二怪:強(qiáng)迫癥晚期誰(shuí)來(lái)救
我們正式進(jìn)入到Go語(yǔ)言的世界,看看Go語(yǔ)言的一些特性,首先以一個(gè)例子開頭:
package main
import(
"os"http://@1:先import進(jìn)來(lái),一會(huì)兒就發(fā)力
"fmt"
)
func main()
{//@2:還是換個(gè)行吧,這樣看起來(lái)帥帥的
x := 1//@3:我先占個(gè)座,一會(huì)來(lái)自習(xí)
fmt.Println("我是第一個(gè)go程序")
}
這是我們的第一個(gè)例子,對(duì)于熟悉其他語(yǔ)言的人來(lái)說(shuō),這看起來(lái)是一個(gè)最正常不過的例子,但是其實(shí)這個(gè)例子中有三處語(yǔ)法錯(cuò)誤,你們先找茬,我先簡(jiǎn)單介紹一下Go語(yǔ)言的基本語(yǔ)法,文件開頭一般要命名一個(gè)包名(package),相同包名即是一家子,如果有package main即為主執(zhí)行程序,通過go install命令即可進(jìn)行安裝,生成可執(zhí)行二進(jìn)制,第二行的import說(shuō)明需要import的包,如果只有一行可以使用類似import "fmt"這種語(yǔ)法,這個(gè)程序的例子多個(gè)import括號(hào)方式會(huì)比較簡(jiǎn)潔,Go語(yǔ)言的函數(shù)是以func開始,局部變量使用:=運(yùn)算符時(shí),編譯器可以自動(dòng)推導(dǎo)出變量的類型。說(shuō)了這么多,我們?cè)摪颜Z(yǔ)法錯(cuò)誤指出來(lái)了,@1這一處錯(cuò)誤是因?yàn)?,在程序文件中,根本沒有使用os包的位置,由于其多余,Go語(yǔ)言強(qiáng)行定義其為語(yǔ)法錯(cuò)誤;@2這一處更加體現(xiàn)了羅布·派克強(qiáng)迫癥晚期患者的癥狀,他要求如第7行的大括號(hào)必須緊跟在main()的后面,否則就是語(yǔ)法錯(cuò)誤;@3和@1類似,這是變量級(jí)別的使用要求,不允許任何變量定義未使用。
羅布·派克定義了大量的規(guī)則用來(lái)保證程序員少犯錯(cuò),這大概是其見過太多C和C++過于自由的導(dǎo)致其難用的最痛的領(lǐng)悟吧。
第三怪:匿名字段真不賴
直接上例子:
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
}
這個(gè)例子講的是Go語(yǔ)言中的struct,Go語(yǔ)言的struct和C++的class比較相似,但是你們可能會(huì)奇怪第@1行的Human是要鬧哪樣,你的對(duì)象呢,你的對(duì)象呢?其實(shí)這里就到Go語(yǔ)言中的匿名字段的神奇之處了。在講匿名字段之前,需要解釋個(gè)事情,Go語(yǔ)言變量命名方式是絕無(wú)僅有的奇葩,聰明的你應(yīng)該已經(jīng)發(fā)現(xiàn)了,這里面變量都是在類型前面,導(dǎo)致如果你給變量附初值時(shí)會(huì)出現(xiàn)var i int=8這種看起來(lái)很欠揍的語(yǔ)法,不過用習(xí)慣也就沒什么了,語(yǔ)言本身就是一堆規(guī)則,大師制定規(guī)則,碼農(nóng)按照規(guī)則拉磨。
我們言歸正傳,繼續(xù)講struct,匿名字段其實(shí)相當(dāng)于在Student使用Human類型時(shí),默認(rèn)編譯器將Human自動(dòng)展開,我們看第@2行即可以發(fā)現(xiàn)Human.name直接過繼給了Student,匿名字段就是可以把兒子變孫子,這種奇葩的事情羅布.派克不是首創(chuàng),咱們中國(guó)老祖宗唐德宗就有認(rèn)自己的孫子為兒子的事兒,這里面有啥八卦隱情這里不表,估計(jì)羅布.派克也不知道這事兒。有的人可能一定要問了,如果倆孫子重名可咋辦,萬(wàn)一孫子跟兒子重名不也亂套了么,這個(gè)你不用著急,如果發(fā)生這種情況,編譯器還是會(huì)及時(shí)發(fā)現(xiàn),星星還是那顆星星,兒子還是那個(gè)兒子,具體可以做實(shí)驗(yàn)去體驗(yàn)吧。
第四怪:interface你陷害
Go語(yǔ)言的interface可以說(shuō)是面向?qū)ο笤O(shè)計(jì)中極其巧妙的實(shí)現(xiàn),其隱含接口實(shí)現(xiàn)不但使得代碼簡(jiǎn)潔,同時(shí)內(nèi)容組織無(wú)與倫比的方便。繼續(xù)讀代碼說(shuō)話:
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)
}
這個(gè)例子略長(zhǎng),前面介紹的struct我們現(xiàn)在已經(jīng)熟悉了,重點(diǎn)看一下interface的定義和實(shí)現(xiàn)。我們可以看到一個(gè)geometry interface的定義,這個(gè)接口里定義了area和perim兩個(gè)方法。重點(diǎn)看一下成員函數(shù)的實(shí)現(xiàn)方法,Go語(yǔ)言成員函數(shù)與struct也是松耦合的,這里我們要重點(diǎn)關(guān)注的是括號(hào),以square的area方法為例,我先看第一個(gè)括號(hào),我們把類型square s稱為這個(gè)方法接收者,其實(shí)就是C++的成員函數(shù)的不同表征方法,在python中,一般是用傳入self來(lái)實(shí)現(xiàn)的。第二括號(hào)才是方法自己的括號(hào),這里我們沒有帶參數(shù),最后一個(gè)float64是返回值類型,如果有多個(gè)返回值需要用括號(hào)括起來(lái)。那么問題來(lái)了,一個(gè)成員方法你最多可以看到多少括號(hào),其實(shí)可能不止三個(gè),但是會(huì)有三部分。
到此有人可能會(huì)問了,interface你陷害,你瞎掰吧,說(shuō)了這么多,還不說(shuō)陷害的事兒。我們言歸正傳,為什么說(shuō)interface陷害呢,因?yàn)橐粋€(gè)struct的方法只要實(shí)現(xiàn)了interface的方法組合,那么Go語(yǔ)言就認(rèn)為我們實(shí)現(xiàn)了這個(gè)接口,這中關(guān)聯(lián)是隱式的,也就是說(shuō),你寫了一個(gè)struct,實(shí)現(xiàn)了一堆方法,可能你就順便實(shí)現(xiàn)了另外一堆接口,這些接口可能連你都不知道。我們看measure方法定義,其接受的參數(shù)是一個(gè)geometry接口,可以直接傳入circle和square對(duì)象,因?yàn)檫@兩個(gè)struct都實(shí)現(xiàn)了area和perim方法,那么他們就實(shí)現(xiàn)了geometry接口,這兩個(gè)struct啥都沒說(shuō),就被陷害說(shuō)他們實(shí)現(xiàn)了geometry接口。
實(shí)際上在fmt.Println接收的參數(shù)就是不定長(zhǎng)interface參數(shù)Stringer,其定義為:type Stringer interface { String() string },由此我們可以看出,只要你的struct實(shí)現(xiàn)了String方法,那么你就實(shí)現(xiàn)了Stringer接口,也就是說(shuō),這樣你就可以打印這個(gè)struct對(duì)象了,這有點(diǎn)兒類似Java中的toString,但是實(shí)現(xiàn)的優(yōu)雅程度就是云泥之別了。順便說(shuō)一句,所有的類型都實(shí)現(xiàn)了空接口interface{},也就是說(shuō),如果一個(gè)函數(shù)參數(shù)為空接口interface{}類型,那么這個(gè)函數(shù)可以接受任何參數(shù),這有點(diǎn)兒像C語(yǔ)言的void,但是Go語(yǔ)言要更加強(qiáng)大。
interface的設(shè)計(jì)是Go語(yǔ)言的神來(lái)之筆,使用過程中你會(huì)越來(lái)越體會(huì)到interface之精妙。
第五怪:加鎖啥的都狗帶
通過通信來(lái)共享內(nèi)存,而非通過共享內(nèi)存來(lái)通信,Go語(yǔ)言原生支持并發(fā),并通過goroutine和channel將并發(fā)編程的簡(jiǎn)潔性和高效性體現(xiàn)的淋漓盡致,我們?cè)僖膊挥脫?dān)心加鎖問題了,在程序員上空死鎖的陰云散去(其實(shí)還是會(huì)寫出死鎖的程序,具體可以查找相關(guān)聊),抬頭再看,并發(fā)編程一片晴空。我們以生產(chǎn)者消費(fèi)者問題舉例,我們會(huì)發(fā)現(xiàn)gorutine實(shí)現(xiàn)并發(fā)是怎樣一種優(yōu)雅:
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都執(zhí)行完退出
}
我們看@1行,我們通過make語(yǔ)法建立了一個(gè)緩沖為6的int管道,生產(chǎn)者只需要關(guān)心生產(chǎn),把生產(chǎn)好的數(shù)據(jù)直接扔進(jìn)管道,消費(fèi)者呢,不用關(guān)心生產(chǎn)者的任何細(xì)節(jié),只需要從管道里取數(shù)據(jù),生產(chǎn)端和消費(fèi)端都是阻塞的,當(dāng)管道為空時(shí),消費(fèi)端阻塞,當(dāng)管道滿時(shí),生產(chǎn)端阻塞。關(guān)鍵字go定義我們新開了一個(gè)goroutine,每個(gè)goroutine你可以理解成線程,但是goroutine更加輕量和高效,一個(gè)程序起成千上萬(wàn)個(gè)goroutine毫無(wú)壓力。
我們剛剛看到有緩沖channel類似于消息隊(duì)列的神勇表現(xiàn),接下來(lái)我們看看無(wú)緩沖channel在多個(gè)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)
}
我們這個(gè)程序用了一個(gè)select關(guān)鍵字,沒錯(cuò),這個(gè)select的作用跟網(wǎng)絡(luò)通信模型的select非常相似,select監(jiān)聽channel中的數(shù)據(jù)流,默認(rèn)select是阻塞的,當(dāng)管道中有發(fā)送或者接收行為時(shí),select才會(huì)執(zhí)行,當(dāng)有多個(gè)管道都準(zhǔn)備好時(shí),select會(huì)從中隨機(jī)取一個(gè)執(zhí)行,這個(gè)程序我們不需要用time.Sleep等待其他goroutine退出,在quit管道被寫入0之后,select偵測(cè)到,執(zhí)行case <-quit后程序退出。
Go語(yǔ)言并發(fā)編程有效利用多核CPU,把比thread更加輕量、高效的goroutine與管道相結(jié)合,極度優(yōu)雅的實(shí)現(xiàn)數(shù)據(jù)共享和同步,加鎖什么的確實(shí)可以狗帶了。
第六怪:靜態(tài)編譯沒依賴
動(dòng)態(tài)鏈接庫(kù)在計(jì)算機(jī)的蠻荒年代起了非常大的作用,那時(shí)的內(nèi)存還是論K的,硬盤是論M的,動(dòng)態(tài)鏈接庫(kù)可以在不同進(jìn)程間通過共享代碼來(lái)節(jié)省內(nèi)存,使用動(dòng)態(tài)鏈接庫(kù)編譯出的二進(jìn)制也非常小,這就使得磁盤空間使用和拷貝代價(jià)很低。但是動(dòng)態(tài)鏈接庫(kù)的缺點(diǎn)也是顯而易見的,甚至因?yàn)閯?dòng)態(tài)鏈接庫(kù)過于混亂產(chǎn)生了專門的名詞,相關(guān)性依賴地獄(dependence hell),不同程序之間的依賴讓無(wú)數(shù)程序員徹夜調(diào)試,而動(dòng)態(tài)鏈接庫(kù)給我們帶來(lái)的好處在今天看來(lái)實(shí)在是不值得的,它為我們節(jié)省的內(nèi)存和磁盤空間實(shí)在微不足道,動(dòng)態(tài)鏈接沖突導(dǎo)致的問題跟帶來(lái)的好處比起來(lái)就太大了,可以說(shuō),現(xiàn)在如果還抱著動(dòng)態(tài)鏈接庫(kù)不放就是丟西瓜撿芝麻完全是得不償失。
Go語(yǔ)言作者羅布·派克顯然也看到了這一點(diǎn),他的解決方案非常簡(jiǎn)單,二進(jìn)制不依賴任何動(dòng)態(tài)鏈接庫(kù),所有的編譯都是靜態(tài)鏈接,我們?cè)僖膊挥脫?dān)心換了一臺(tái)機(jī)器運(yùn)行程序無(wú)法執(zhí)行的問題了,這種做法只是損失了很少的內(nèi)存和磁盤空間,但是帶來(lái)整體性(integrity)的極大好處。
第七怪:編碼運(yùn)行真是快
有人把Go語(yǔ)言稱為21世紀(jì)的C語(yǔ)言,跟C語(yǔ)言相比,Go語(yǔ)言的運(yùn)行效率當(dāng)之無(wú)愧,但是如果把C語(yǔ)言的編碼效率與Go語(yǔ)言相比,C語(yǔ)言會(huì)被甩出好幾條街,可以這么說(shuō),Go語(yǔ)言是python和C的合體,它兼顧了C語(yǔ)言的運(yùn)行效率和python的編碼效率。Go語(yǔ)言的關(guān)鍵字只有25個(gè),Go語(yǔ)言的所有循環(huán)的寫法只有一個(gè)for把其他語(yǔ)言的while、foreach等一堆亂七八糟的命名整合成一個(gè),使用極其簡(jiǎn)潔。其運(yùn)行效率到底有多高呢,下圖是benchmarksgame網(wǎng)站上對(duì)比Go語(yǔ)言和C執(zhí)行不同算法執(zhí)行效率,除了個(gè)別算法運(yùn)行效率差別較大,大部分算法Go語(yǔ)言執(zhí)行效率跟C相差可以忽略不計(jì)。

我們這種對(duì)比雖然可能不一定公平,我們可以直觀上感受Go語(yǔ)言運(yùn)行的高效。
第八怪:異常處理沒有try
“作為現(xiàn)代編程語(yǔ)言一枚,try-catch都沒有,你還想讓我在編程界混么”,Go語(yǔ)言的內(nèi)心OS一定是這樣。現(xiàn)實(shí)是羅布·派克根本不想讓try-catch出現(xiàn),原因我們來(lái)看一段python代碼:
def main():
try:
check_filename()
check_filesize()
check_filelines()
read_file()
except:
exit(1)
#endf main
if __name__ == "__init__":
main()
可能有人會(huì)說(shuō),這代碼挺正常啊,出問題就退出唄,這么寫代碼的人真是被python給慣的太懶了,多少行代碼都能用一個(gè)try-catch給包起來(lái),根本不仔細(xì)考量到底可能會(huì)發(fā)生哪些異常,這些所謂的異??赡芨静皇钱惓?,曾經(jīng)見過在python有人用try-catch代替if-else來(lái)對(duì)類似于字段個(gè)數(shù)判斷處理,這種程序執(zhí)行結(jié)果是沒有錯(cuò),但是無(wú)論是執(zhí)行效率還是代碼可讀性以及代碼可維護(hù)性上都會(huì)有問題。羅布·派克強(qiáng)制讓編碼人員仔細(xì)考慮邏輯,對(duì)于可以預(yù)期的異常就不應(yīng)該用try-catch來(lái)處理,直接就是代碼處理邏輯分支,而不可預(yù)期的異常就應(yīng)該拋出來(lái),所以在Go語(yǔ)言中遇到數(shù)組越界程序就會(huì)直接painc程序退出,另外Go語(yǔ)言還有個(gè)recover機(jī)制,通過調(diào)用recover捕獲到panic的輸入值,恢復(fù)正常的執(zhí)行。
沒有try-catch機(jī)制實(shí)際上是對(duì)程序員的嚴(yán)格要求,這也是對(duì)程序的一種保護(hù)。panic和recover在使用時(shí)都應(yīng)該慎之又慎,作為程序員的我們最重要的是嚴(yán)謹(jǐn)?shù)目紤]代碼邏輯,盡量避免panic。
回顧
本文從8個(gè)不同角度介紹了Go語(yǔ)言的由來(lái)和設(shè)計(jì),它們分別是:
第一怪:老朽偽裝小鮮肉
第二怪:強(qiáng)迫癥晚期誰(shuí)來(lái)救
第三怪:匿名字段真不賴
第四怪:interface你陷害
第五怪:加鎖啥的都狗帶
第六怪:靜態(tài)編譯沒依賴
第七怪:編碼運(yùn)行真是快
第八怪:異常處理沒有try
總結(jié)
Go語(yǔ)言作者羅布·派克作為貝爾實(shí)驗(yàn)室Unix先驅(qū),看慣了各種編程語(yǔ)言刀光劍影、鼓角爭(zhēng)鳴,各種語(yǔ)言你方唱罷我登場(chǎng),它們雖然在一定程度上解決了之前使用語(yǔ)言的一些弊端,如C++語(yǔ)言把面向?qū)ο笠M(jìn),Java把設(shè)計(jì)模式發(fā)揚(yáng)光大,python使寫代碼接近自然語(yǔ)言,但是他們的時(shí)代畢竟要過去,Go語(yǔ)言簡(jiǎn)潔的語(yǔ)法,原生的并發(fā)支持,interface是精妙設(shè)計(jì)無(wú)一不顯示了作者對(duì)編程語(yǔ)言的全新的設(shè)計(jì)和理解,羅布·派克把我們帶入了一個(gè)新的編程語(yǔ)言世界,在這里,我們用簡(jiǎn)單嚴(yán)謹(jǐn)?shù)恼Z(yǔ)法書寫程序藝術(shù),而Go語(yǔ)言以其高效運(yùn)行來(lái)回報(bào)我們,生活在Go語(yǔ)言的世界里是程序員的幸福,用時(shí)下流行的話來(lái)說(shuō),用Go語(yǔ)言編程的人運(yùn)氣都不會(huì)太壞。
注:本文大部分代碼來(lái)自謝孟軍的《Go web編程》一書。