這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
Golang從09年發布,中間經曆了多個版本的演化,已經漸漸趨於成熟,並且出現了很多優秀的開源項目,比如我們熟知的docker,etcd,kubernetes等,其媲美於C的效能、Python的開發效率,又被稱為21世紀的C語言,尤其適合開發後台服務。這篇文章主要是介紹Golang的一些主要特性,和Java做一個對比,以便更好的理解Golang這門語言。
關於Golang環境的搭建就不講了,可以參考官方文檔或者Google一下,配置下SDK和PATH即可,非常簡單,我們就從Go版本的Hello World開始
Hello World
每種語言都有自己的Hello World,Go也不例外,Go版本的如下:
1234567 |
package mainimport "fmt"func main() {fmt.Println("Hello, 世界")} |
我們使用go run運行後,會在控制台終端看到Hello, 世界的輸出。我們來看下這段代碼:
- package 是一個關鍵字,定義一個包,和Java裡的package一樣,也是模組化的關鍵。
- main包是一個特殊的包名,它表示當前是一個可執行程式,而不是一個庫。
- import 也是一個關鍵字,表示要引入的包,和Java的import關鍵字一樣,引入後才可以使用它。
- fmt是一個包名,這裡表示要引入fmt這個包,這樣我們就可以使用它的函數了。
- main函數是主函數,表示程式執行的入口,Java也有同名函數,但是多了一個String[]類型的參數。
- Println是fmt包裡的函數,和Java裡的system.out.println作用類似,這裡輸出一段文字。
整段代碼非常簡潔,關鍵字、函數、包等和Java非常相似,不過注意,go是不需要以;(分號)結尾的。
變數
go語言變數的聲明和java的略有不同,以聲明一個int類型,變數名為age為例,go語言變數產生如下:
同樣的變數,在java中的聲明是:
可以看到go的變數聲明,修飾變數的類型在變數的後面,而且是以var關鍵字開頭。
最後面的賦值可以在聲明的時候忽略,這樣變數就有一個預設的值,稱之為零值。零值是一個統稱,以類型而定,比如int類型的零值為0,string類型的零值是””Null 字元串。
在go中除了以var聲明變數之外,還有一種簡短的變數聲明方式:=,比如上面例子,可以如下簡單聲明:
這種方式和上面的例子等價,但是少了var和變數類型,所以簡短方便,用的多。使用這種方式,變數的類型由go根據值推匯出來,比如這裡預設是int。
常量
有了變數,就少不了常量,和var關鍵字不一樣,go的常量使用const聲明,這個和C裡的常量一樣。
這樣就聲明了一個常量age,其值是10,因為我們這裡沒有指定常量的類型,所以常量的類型是根據值推匯出來的。所以等價的我們也可以指定常量類型,如下:
相比來說,java下的常量定義就要複雜一些,要有static final修飾符,才是常量:
1 |
private static final int AGE = 10; |
這個和go的實現等價,但是它的定義修飾符比go多多了,而且常量類型不能省略。
大小寫標記存取權限
我們上面的go例子中我特意用了小些的變數名age,甚至常量我也沒有寫成AGE,但是在java中,對於常量我們的習慣是全部大些。
在go中不能隨便使用大小寫問題,是因為大小寫具有特殊意義,在go中,大些字母開頭的變數或者函數等是public的,可以被其他包訪問;小些的則是private的,不能被其他包訪問到。這樣就省去了public和private聲明的煩惱,使代碼變的更簡潔。
特別說明,這些匯出規則只適用於包層級名字定義,不能使函數內部的定義。
包
包的規則和java很像,每個包都有自己獨立的空間,所以可以用來做模組化,封裝,組織代碼等。
和java不同的是,go的包裡可以有函數,比如我們常用的fmt.Println(),但是在在java中沒有這種用法,java的方法必須是屬於一個類或者類的執行個體的。
要使用一個包,就需要先匯入,使用import關鍵字,和java也一樣,可以參見前面的helloworld樣本。
如果我們需要匯入多個包的時候,可以像java一樣,一行行匯入,也可以使用捷徑一次匯入,這個是java所沒有的。
123456 |
import ("io""log""net""strconv") |
類型轉換
go對於變數的類型有嚴格的限制,不同類型之間的變數不能進行賦值、運算式等操作,必須要要轉換成同一類型才可以,比如int32和int64兩種int類型的變數不能直接相加,要轉換成一樣才可以。
1234 |
var a int32 = 13var b int64 = 20c := int64(a) + b |
這種限制主要是防止我們誤操作,導致一些莫名其妙的問題。在java中因為有自動轉型的概念,所以可以不同類型的可以進行操作,比如int可以和double相加,int類型可以通過+和字串拼接起來,這些在go中都是不可行的。
map
map類型,Java裡是Map介面,go裡叫做字典,因為其常用,在go中,被最佳化為一個語言上支援的結構,原生支援,就像一個關鍵字一樣,而不是java裡的要使用內建的sdk集合庫,比如HashMap等。
123456 |
ages := make(map[string]int)ages["linday"] = 20ages["michael"] = 30fmt.Print(ages["michael"]) |
go裡要建立一個map對應,需要使用關鍵字make,然後就可以對這個map進行操作。
map的結構也非常簡單,符合KV模型,定義為map[key]value, 方括弧裡是key的類型,方括弧外緊跟著對應的value的類型,這些明顯和Java的Map介面不同。如果在go中我們要刪除map中的一個元素怎麼辦?使用內建的delete函數就可以,如下代碼刪除ages這個map中,key為michael的元素。
如果我們想遍曆map中的KV值怎麼辦?答案是使用range風格的for迴圈,可比Java Map的遍曆簡潔多了。
123 |
for name,age := range ages {fmt.Println("name:",name,",age:",age)} |
range一個map,會返回兩個值,第一個是key,第二個是value,這個也是go多值返回的優勢,下面會講。
函數方法
在go中,函數和方法是不一樣的,我們一般稱包層級的(直接可以通過包調用的)稱之為函數,比如fmt.Println();把和一個類型關聯起來的函數稱之為方法,如下樣本:
12345678910111213141516 |
package libimport "time"type Person struct {age intname string}func (p Person) GetName() string {return p.name}func GetTime() time.Time{return time.Now()} |
其中GetTime()可以通過lib.GetTime()直接調用,稱之為函數;而GetName()則屬於Person這個結構體的函數,只能聲明了Person類型的執行個體後才可以調用,稱之為方法。
不管是函數還是方法,定義是一摸一樣的。而在這裡,最可以講的就是多值返回,也就是可以同時返回多個值,這就大大為我們帶來了方便,比如上個遍曆map的例子,直接可以擷取KV,如果只能返回一個值,我們就需要調用兩次方法才可以。
123 |
func GetTime() (time.Time,error){return time.Now(),nil} |
多值返回也很簡單,返回的值使用逗號隔開即可。如果要接受多值的返回,也需要以逗號分隔的變數,有幾個傳回值,就需要幾個變數,比如這裡:
如果有個傳回值,我們用不到,不想浪費一個變數接收怎麼辦?這時候可以使用空標誌符_,這是java沒有的。
指標
Go的指標和C中的聲明定義是一樣的,其作用類似於Java引用變數效果。
1234 |
var age int = 10var p *int = &age*p = 11fmt.Println(age) |
其中指標p指向變數age的記憶體位址,如果修改*p的值,那麼變數age的值也同時會被修改,例子中列印出來的值為11,而不是10.
相對應java參考型別的變數,可以理解為一個HashMap類型的變數,這個變數傳遞給一個方法,在該方法裡對HashMap修改,刪除,就會影響原來的HashMap。引用變數集合類最容易理解,自己的類也可以,不過基本類型不行,基本類型不是參考型別的,他們在方法傳參的時候,是拷貝的值。
結構體替代類
Go中沒有類型的概念,只有結構體,這個和C是一樣的。
1234 |
type Person struct {age intname string} |
Go中的結構體是不能定義方法的,只能是變數,這點和Java不一樣的,如果要訪問結構體內的成員變數,通過.操作符即可。
123 |
func (p Person) GetName() string {return p.name} |
這就是通過.操作符訪問變數的方式,同時它也是一個為結構體定義方法的例子,和函數不一樣的是,在func關鍵字後要執行該方法的接收者,這個方法就是屬於這個接收者,例子中是Person這個結構體。
在Go中如果想像Java一樣,讓一個結構體繼承另外一個結構體怎麼辦?也有辦法,不過在Go中稱之為組合或者嵌入。
123456789 |
type Person struct {age intname stringAddress}type Address struct {city string} |
結構體Address被嵌入了Person中,這樣Person就擁有了Address的變數和方法,就想自己的一樣,這就是組合的威力。通過這種方式,我們可以把簡單的對象組合成複雜的對象,並且他們之間沒有強約束關係,Go倡導的是組合,而不是繼承、多態。
介面
Go的介面和Java類型,不過它不需要強制實現,在Go中,如果你這個類型(基本類型,結構體等都可以)擁有了介面的所有方法,那麼就預設為這個類型實現了這個介面,是隱式的,不需要和java一樣,強制使用implement強制實現。
1234567 |
type Stringer interface {String() string}func (p Person) String() string {return "name is "+p.name+",age is "+strconv.Itoa(p.age)} |
以上執行個體中可以看到,Person這個結構體擁有了fmt.Stringer介面的方法,那麼就說明Person實現了fmt.Stringer介面。
介面也可以像結構體一樣組合嵌套,這裡不再贅述。
並發
Go並發主要靠go goroutine支援,也稱之為go協程或者go程,他是語言層面支援的,非常輕量級的多任務支援,也可以把他簡單的理解為java語言的線程,不過是不一樣的。
這就啟動一個goroutine來執行run函數,代碼非常簡潔,如果在java中,需要先New一個Thread,然後在重寫他的run方法,然後在start才可以開始。
兩個goroutine可以通過channel來通訊,channel是一個特殊的類型,也是go語言層級上的支援,他類似於一個管道,可以儲存資訊,也可以從中讀取資訊。
12345678910111213141516 |
package mainimport "fmt"func main() {result:=make(chan int)go func() {sum:=0for i:=0;i<10;i++{sum=sum+i}result<-sum}()fmt.Print(<-result)} |
以上樣本使用一個單獨的goroutine求和,當得到結果時,存放在result這個chan裡,然後供main goroutine讀取出來。當result沒有被儲存值的時候,讀取result是阻塞的,所以會等到結果返回,協同工作,通過chan通訊。
對於並發,go還提供了一套同步機制,都在sync包裡,有鎖,有一些常用的工具函數等,和java的concurrent架構差不多。
異常機制
相比java的Exception來說,go有兩種機制,不過最常用的還是error錯誤類型,panic只用於嚴重的錯誤。
123 |
type error interface { Error() string} |
go內建的error類型非常簡潔,只用實現Error方法即可,可以列印一些詳細的錯誤資訊,比如常見的函數多值返回,最後一個傳回值經常是error,用於傳遞一些錯誤問題,這種方式要比java throw Exception的方法更優雅。
Defer代替finally
go中沒有java的finally了,那麼如果我們要關閉一些一些串連,檔案流等怎麼辦呢,為此go為我們提供了defer關鍵字,這樣就可以保證永遠被執行到,也就不怕關閉不了串連了。
123 |
f,err:=os.Open(filename)defer f.Close()readAll(f) |
統一編碼風格
在編碼中,我們有時為了是否空行,大括弧是否獨佔一行等編碼風格問題爭論不休,到了Go這裡就終止了,因為go是強制的,比如花括弧不能獨佔一行,比如定義的變數必須使用,否則就不能編譯通過。
第二種就是go fmt這個工具提供的非強制性規範,雖然不是強制的,不過也建議使用,這樣整個團隊的代碼看著就像一個人寫的。很多go代碼編輯器都提供儲存時自動gofmt格式的話,所以效率也非常高。
便捷的部署
go最終產生的是一個可執行檔,不管你的程式依賴多少庫,都會被打包進行,產生一個可執行檔,所以相比java龐大的jar庫來說,他的部署非常方便,執行運行這個可執行檔就好了。
對於Web開發,更方便,不用安裝jdk,tomcat容器等等這些環境,直接一個可執行檔,就啟動了。對於go這種便捷的部署方式,我覺得他更能推進docker的服務化,因為docker就是倡導一個執行個體一個服務,而且不用各種依賴,layer層級又沒那麼多,docker image也會小很多。
最後,go目前已經在TIOBE語言熱門排行榜上名列13名了,上升速度還是非常快的,而且隨著服務化,容器化,他的優勢會越來越多的顯現出來,得到更廣泛的應用。
如果你感興趣,那麼開始吧,提前準備,機會來的時候,就不會錯過了。