本文可以隨意轉(zhuǎn)載蜡塌,轉(zhuǎn)載請標(biāo)明作者和來源邻薯。
引子
本文題目凸顯一個‘怪’字,怪即為奇異孵构,Go語言很多特點和特性可稱之為奇異立莉,掌握了這些奇異之處绢彤,我們自然也就了解了Go語言的精髓。
概述
Go語言作為一門新時代的編譯語言蜓耻,以排山倒海之勢迅速占領(lǐng)后臺服務(wù)開發(fā)陣地茫舶,在C++標(biāo)準(zhǔn)委員會急不可耐的把原本就極其復(fù)雜的C++語言用C++11變的更加復(fù)雜以后,作為面向過程刹淌、面向?qū)ο笕氖稀⒎盒途幊陶Z言的C++逐漸被追求最新技術(shù)的程序員所拋棄,而Go語言以其無比簡單的語法有勾,極其高效的運行效率獲得了越來越多人的親睞疹启,Go語言原生支持并發(fā)的特性使其在現(xiàn)今的并行計算時代更具有非凡意義。
本文主旨
本文主要通過簡單的幾個例子的介紹蔼卡,為讀者構(gòu)建一個Go語言的基本印象喊崖,本文不是Go語言教程,作者在這里僅用調(diào)侃的態(tài)度來給沒有接觸過Go語言的讀者提供一些Go語言特性的信息雇逞,希望在看到這些特性后荤懂,我能欣喜的看到有人可以捧起書真正進(jìn)入Go語言的世界。
第一怪:老朽偽裝小鮮肉
這么是說誰呢塘砸,其實呢节仿,我去搜索Go語言介紹的時候,驚人的發(fā)現(xiàn)一個萌萌的年輕人坐在那里掉蔬,然后我就以為這樣一個小鮮肉竟然發(fā)明了Go語言廊宪,真是“江山代有才人出,后浪死在沙灘上”女轿。但是別急箭启,再繼續(xù)搜索后我發(fā)現(xiàn),Go語言的創(chuàng)造者羅布·派克(Rob Pike)其實早就名聲斐然谈喳,出生于1965年的他并不年輕,他可是UTF-8設(shè)計者戈泼,同時羅布·派克跟Unix淵源極深婿禽,他和Ken Thompson以及 Dennis M.Ritche一起開發(fā)了Unix操作系統(tǒng)赏僧,作為資深Geek,羅布·派克不忘在體育界刷存在感扭倾,他在1980年奧運會上轉(zhuǎn)了一圈淀零,拿了個射箭銀牌。閑著沒事他還在天文學(xué)那邊兒插一腳膛壹,真是程序員大師中的一朵奇葩驾中。
順便說一句,由于Go語言在Google大行其勢模聋,原來的香餑餑Python之父吉多·范羅蘇姆(Guido Van Rossum) 于2012年底黯然離開谷歌加入到了Dropbox肩民,這也暗示著python時代將要終結(jié),Go語言的時代正在到來链方。
第二怪:強(qiáng)迫癥晚期誰來救
我們正式進(jìn)入到Go語言的世界持痰,看看Go語言的一些特性,首先以一個例子開頭:
package main
import(
"os"http://@1:先import進(jìn)來祟蚀,一會兒就發(fā)力
"fmt"
)
func main()
{//@2:還是換個行吧工窍,這樣看起來帥帥的
x := 1//@3:我先占個座,一會來自習(xí)
fmt.Println("我是第一個go程序")
}
這是我們的第一個例子前酿,對于熟悉其他語言的人來說患雏,這看起來是一個最正常不過的例子,但是其實這個例子中有三處語法錯誤罢维,你們先找茬淹仑,我先簡單介紹一下Go語言的基本語法,文件開頭一般要命名一個包名(package)言津,相同包名即是一家子攻人,如果有package main
即為主執(zhí)行程序,通過go install
命令即可進(jìn)行安裝悬槽,生成可執(zhí)行二進(jìn)制怀吻,第二行的import
說明需要import
的包,如果只有一行可以使用類似import "fmt"
這種語法初婆,這個程序的例子多個import
括號方式會比較簡潔蓬坡,Go語言的函數(shù)是以func
開始,局部變量使用:=
運算符時磅叛,編譯器可以自動推導(dǎo)出變量的類型屑咳。說了這么多,我們該把語法錯誤指出來了弊琴,@1這一處錯誤是因為兆龙,在程序文件中,根本沒有使用os包的位置敲董,由于其多余紫皇,Go語言強(qiáng)行定義其為語法錯誤慰安;@2這一處更加體現(xiàn)了羅布·派克強(qiáng)迫癥晚期患者的癥狀,他要求如第7行的大括號必須緊跟在main()
的后面聪铺,否則就是語法錯誤化焕;@3和@1類似,這是變量級別的使用要求铃剔,不允許任何變量定義未使用撒桨。
羅布·派克定義了大量的規(guī)則用來保證程序員少犯錯,這大概是其見過太多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
}
這個例子講的是Go語言中的struct凤类,Go語言的struct
和C++的class
比較相似,但是你們可能會奇怪第@1行的Human
是要鬧哪樣蝶押,你的對象呢踱蠢,你的對象呢?其實這里就到Go語言中的匿名字段的神奇之處了棋电。在講匿名字段之前茎截,需要解釋個事情,Go語言變量命名方式是絕無僅有的奇葩赶盔,聰明的你應(yīng)該已經(jīng)發(fā)現(xiàn)了企锌,這里面變量都是在類型前面,導(dǎo)致如果你給變量附初值時會出現(xiàn)var i int=8
這種看起來很欠揍的語法于未,不過用習(xí)慣也就沒什么了撕攒,語言本身就是一堆規(guī)則,大師制定規(guī)則烘浦,碼農(nóng)按照規(guī)則拉磨抖坪。
我們言歸正傳,繼續(xù)講struct
闷叉,匿名字段其實相當(dāng)于在Student
使用Human
類型時擦俐,默認(rèn)編譯器將Human
自動展開,我們看第@2行即可以發(fā)現(xiàn)Human.name
直接過繼給了Student
握侧,匿名字段就是可以把兒子變孫子蚯瞧,這種奇葩的事情羅布.派克不是首創(chuàng),咱們中國老祖宗唐德宗就有認(rèn)自己的孫子為兒子的事兒品擎,這里面有啥八卦隱情這里不表埋合,估計羅布.派克也不知道這事兒。有的人可能一定要問了萄传,如果倆孫子重名可咋辦甚颂,萬一孫子跟兒子重名不也亂套了么,這個你不用著急,如果發(fā)生這種情況振诬,編譯器還是會及時發(fā)現(xiàn)瓣铣,星星還是那顆星星,兒子還是那個兒子贷揽,具體可以做實驗去體驗吧。
第四怪:interface你陷害
Go語言的interface
可以說是面向?qū)ο笤O(shè)計中極其巧妙的實現(xiàn)梦碗,其隱含接口實現(xiàn)不但使得代碼簡潔禽绪,同時內(nèi)容組織無與倫比的方便。繼續(xù)讀代碼說話:
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
我們現(xiàn)在已經(jīng)熟悉了印屁,重點看一下interface
的定義和實現(xiàn)。我們可以看到一個geometry interface
的定義斩例,這個接口里定義了area
和perim
兩個方法雄人。重點看一下成員函數(shù)的實現(xiàn)方法,Go語言成員函數(shù)與struct
也是松耦合的念赶,這里我們要重點關(guān)注的是括號础钠,以square
的area
方法為例,我先看第一個括號叉谜,我們把類型square s
稱為這個方法接收者旗吁,其實就是C++的成員函數(shù)的不同表征方法,在python中停局,一般是用傳入self來實現(xiàn)的很钓。第二括號才是方法自己的括號,這里我們沒有帶參數(shù)董栽,最后一個float64
是返回值類型码倦,如果有多個返回值需要用括號括起來。那么問題來了锭碳,一個成員方法你最多可以看到多少括號袁稽,其實可能不止三個,但是會有三部分工禾。
到此有人可能會問了运提,interface
你陷害,你瞎掰吧闻葵,說了這么多民泵,還不說陷害的事兒。我們言歸正傳槽畔,為什么說interface
陷害呢栈妆,因為一個struct
的方法只要實現(xiàn)了interface
的方法組合,那么Go語言就認(rèn)為我們實現(xiàn)了這個接口,這中關(guān)聯(lián)是隱式的鳞尔,也就是說嬉橙,你寫了一個struct
,實現(xiàn)了一堆方法寥假,可能你就順便實現(xiàn)了另外一堆接口市框,這些接口可能連你都不知道。我們看measure
方法定義糕韧,其接受的參數(shù)是一個geometry
接口枫振,可以直接傳入circle
和square
對象,因為這兩個struct
都實現(xiàn)了area
和perim
方法萤彩,那么他們就實現(xiàn)了geometry
接口粪滤,這兩個struct
啥都沒說,就被陷害說他們實現(xiàn)了geometry
接口雀扶。
實際上在fmt.Println
接收的參數(shù)就是不定長interface
參數(shù)Stringer
杖小,其定義為:type Stringer interface { String() string }
,由此我們可以看出愚墓,只要你的struct實現(xiàn)了String
方法予权,那么你就實現(xiàn)了Stringer
接口,也就是說浪册,這樣你就可以打印這個struct
對象了伟件,這有點兒類似Java
中的toString
,但是實現(xiàn)的優(yōu)雅程度就是云泥之別了议经。順便說一句斧账,所有的類型都實現(xiàn)了空接口interface{}
,也就是說煞肾,如果一個函數(shù)參數(shù)為空接口interface{}
類型咧织,那么這個函數(shù)可以接受任何參數(shù),這有點兒像C語言的void
籍救,但是Go語言要更加強(qiáng)大习绢。
interface
的設(shè)計是Go語言的神來之筆,使用過程中你會越來越體會到interface之精妙蝙昙。
第五怪:加鎖啥的都狗帶
通過通信來共享內(nèi)存闪萄,而非通過共享內(nèi)存來通信,Go語言原生支持并發(fā)奇颠,并通過goroutine
和channel
將并發(fā)編程的簡潔性和高效性體現(xiàn)的淋漓盡致败去,我們再也不用擔(dān)心加鎖問題了,在程序員上空死鎖的陰云散去(其實還是會寫出死鎖的程序烈拒,具體可以查找相關(guān)聊)圆裕,抬頭再看广鳍,并發(fā)編程一片晴空。我們以生產(chǎn)者消費者問題舉例吓妆,我們會發(fā)現(xiàn)gorutine實現(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
語法建立了一個緩沖為6的int
管道,生產(chǎn)者只需要關(guān)心生產(chǎn)行拢,把生產(chǎn)好的數(shù)據(jù)直接扔進(jìn)管道祖秒,消費者呢,不用關(guān)心生產(chǎn)者的任何細(xì)節(jié)舟奠,只需要從管道里取數(shù)據(jù)狈涮,生產(chǎn)端和消費端都是阻塞的,當(dāng)管道為空時鸭栖,消費端阻塞,當(dāng)管道滿時握巢,生產(chǎn)端阻塞晕鹊。關(guān)鍵字go
定義我們新開了一個goroutine
,每個goroutine
你可以理解成線程暴浦,但是goroutine
更加輕量和高效溅话,一個程序起成千上萬個goroutine毫無壓力。
我們剛剛看到有緩沖channel
類似于消息隊列的神勇表現(xiàn)歌焦,接下來我們看看無緩沖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
關(guān)鍵字飞几,沒錯,這個select
的作用跟網(wǎng)絡(luò)通信模型的select
非常相似独撇,select
監(jiān)聽channel
中的數(shù)據(jù)流屑墨,默認(rèn)select
是阻塞的,當(dāng)管道中有發(fā)送或者接收行為時纷铣,select
才會執(zhí)行卵史,當(dāng)有多個管道都準(zhǔn)備好時,select
會從中隨機(jī)取一個執(zhí)行搜立,這個程序我們不需要用time.Sleep
等待其他goroutine
退出以躯,在quit
管道被寫入0之后,select
偵測到啄踊,執(zhí)行case <-quit
后程序退出忧设。
Go語言并發(fā)編程有效利用多核CPU,把比thread
更加輕量颠通、高效的goroutine
與管道相結(jié)合址晕,極度優(yōu)雅的實現(xiàn)數(shù)據(jù)共享和同步,加鎖什么的確實可以狗帶了顿锰。
第六怪:靜態(tài)編譯沒依賴
動態(tài)鏈接庫在計算機(jī)的蠻荒年代起了非常大的作用斩箫,那時的內(nèi)存還是論K的吏砂,硬盤是論M的,動態(tài)鏈接庫可以在不同進(jìn)程間通過共享代碼來節(jié)省內(nèi)存乘客,使用動態(tài)鏈接庫編譯出的二進(jìn)制也非常小狐血,這就使得磁盤空間使用和拷貝代價很低。但是動態(tài)鏈接庫的缺點也是顯而易見的易核,甚至因為動態(tài)鏈接庫過于混亂產(chǎn)生了專門的名詞匈织,相關(guān)性依賴地獄(dependence hell),不同程序之間的依賴讓無數(shù)程序員徹夜調(diào)試牡直,而動態(tài)鏈接庫給我們帶來的好處在今天看來實在是不值得的缀匕,它為我們節(jié)省的內(nèi)存和磁盤空間實在微不足道,動態(tài)鏈接沖突導(dǎo)致的問題跟帶來的好處比起來就太大了碰逸,可以說乡小,現(xiàn)在如果還抱著動態(tài)鏈接庫不放就是丟西瓜撿芝麻完全是得不償失。
Go語言作者羅布·派克顯然也看到了這一點饵史,他的解決方案非常簡單满钟,二進(jìn)制不依賴任何動態(tài)鏈接庫,所有的編譯都是靜態(tài)鏈接胳喷,我們再也不用擔(dān)心換了一臺機(jī)器運行程序無法執(zhí)行的問題了湃番,這種做法只是損失了很少的內(nèi)存和磁盤空間,但是帶來整體性(integrity)的極大好處吭露。
第七怪:編碼運行真是快
有人把Go語言稱為21世紀(jì)的C語言吠撮,跟C語言相比,Go語言的運行效率當(dāng)之無愧讲竿,但是如果把C語言的編碼效率與Go語言相比泥兰,C語言會被甩出好幾條街,可以這么說题禀,Go語言是python和C的合體逾条,它兼顧了C語言的運行效率和python的編碼效率。Go語言的關(guān)鍵字只有25個投剥,Go語言的所有循環(huán)的寫法只有一個for把其他語言的while师脂、foreach等一堆亂七八糟的命名整合成一個,使用極其簡潔江锨。其運行效率到底有多高呢吃警,下圖是benchmarksgame網(wǎng)站上對比Go語言和C執(zhí)行不同算法執(zhí)行效率,除了個別算法運行效率差別較大啄育,大部分算法Go語言執(zhí)行效率跟C相差可以忽略不計酌心。
我們這種對比雖然可能不一定公平,我們可以直觀上感受Go語言運行的高效挑豌。
第八怪:異常處理沒有try
“作為現(xiàn)代編程語言一枚安券,try-catch
都沒有墩崩,你還想讓我在編程界混么”,Go語言的內(nèi)心OS一定是這樣『蠲悖現(xiàn)實是羅布·派克根本不想讓try-catch
出現(xiàn)鹦筹,原因我們來看一段python代碼:
def main():
try:
check_filename()
check_filesize()
check_filelines()
read_file()
except:
exit(1)
#endf main
if __name__ == "__init__":
main()
可能有人會說,這代碼挺正常啊址貌,出問題就退出唄铐拐,這么寫代碼的人真是被python給慣的太懶了,多少行代碼都能用一個try-catch
給包起來练对,根本不仔細(xì)考量到底可能會發(fā)生哪些異常遍蟋,這些所謂的異常可能根本不是異常螟凭,曾經(jīng)見過在python有人用try-catch
代替if-else
來對類似于字段個數(shù)判斷處理虚青,這種程序執(zhí)行結(jié)果是沒有錯,但是無論是執(zhí)行效率還是代碼可讀性以及代碼可維護(hù)性上都會有問題螺男。羅布·派克強(qiáng)制讓編碼人員仔細(xì)考慮邏輯棒厘,對于可以預(yù)期的異常就不應(yīng)該用try-catch
來處理,直接就是代碼處理邏輯分支烟号,而不可預(yù)期的異常就應(yīng)該拋出來,所以在Go語言中遇到數(shù)組越界程序就會直接painc
程序退出政恍,另外Go語言還有個recover
機(jī)制汪拥,通過調(diào)用recover
捕獲到panic
的輸入值,恢復(fù)正常的執(zhí)行篙耗。
沒有try-catch
機(jī)制實際上是對程序員的嚴(yán)格要求迫筑,這也是對程序的一種保護(hù)。panic
和recover
在使用時都應(yīng)該慎之又慎宗弯,作為程序員的我們最重要的是嚴(yán)謹(jǐn)?shù)目紤]代碼邏輯脯燃,盡量避免panic
。
回顧
本文從8個不同角度介紹了Go語言的由來和設(shè)計蒙保,它們分別是:
第一怪:老朽偽裝小鮮肉
第二怪:強(qiáng)迫癥晚期誰來救
第三怪:匿名字段真不賴
第四怪:interface你陷害
第五怪:加鎖啥的都狗帶
第六怪:靜態(tài)編譯沒依賴
第七怪:編碼運行真是快
第八怪:異常處理沒有try
總結(jié)
Go語言作者羅布·派克作為貝爾實驗室Unix先驅(qū)辕棚,看慣了各種編程語言刀光劍影、鼓角爭鳴邓厕,各種語言你方唱罷我登場逝嚎,它們雖然在一定程度上解決了之前使用語言的一些弊端,如C++語言把面向?qū)ο笠M(jìn)详恼,Java把設(shè)計模式發(fā)揚光大补君,python使寫代碼接近自然語言,但是他們的時代畢竟要過去昧互,Go語言簡潔的語法挽铁,原生的并發(fā)支持伟桅,interface
是精妙設(shè)計無一不顯示了作者對編程語言的全新的設(shè)計和理解,羅布·派克把我們帶入了一個新的編程語言世界叽掘,在這里楣铁,我們用簡單嚴(yán)謹(jǐn)?shù)恼Z法書寫程序藝術(shù),而Go語言以其高效運行來回報我們够掠,生活在Go語言的世界里是程序員的幸福民褂,用時下流行的話來說,用Go語言編程的人運氣都不會太壞疯潭。
注:本文大部分代碼來自謝孟軍的《Go web編程》一書赊堪。