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版本的如下:
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語言變量生成如下:
var age int =10
同樣的變量,在java中的聲明是:
int age = 10;
可以看到go的變量聲明,修飾變量的類型在變量的後面,而且是以var關鍵字開頭。
var 變量名 類型 = 表達式
最後面的賦值可以在聲明的時候忽略,這樣變量就有一個默認的值,稱之為零值。零值是一個統稱,以類型而定,比如int類型的零值為0,string類型的零值是」」空字符串。
在go中除了以var聲明變量之外,還有一種簡短的變量聲明方式:=,比如上面例子,可以如下簡單聲明:
age := 10
這種方式和上面的例子等價,但是少了var和變量類型,所以簡短方便,用的多。使用這種方式,變量的類型由go根據值推導出來,比如這裡默認是int。
常量有了變量,就少不了常量,和var關鍵字不一樣,go的常量使用const聲明,這個和C裡的常量一樣。
const age = 10
這樣就聲明了一個常量age,其值是10,因為我們這裡沒有指定常量的類型,所以常量的類型是根據值推導出來的。所以等價的我們也可以指定常量類型,如下:
const age int = 10
相比來說,java下的常量定義就要複雜一些,要有static final修飾符,才是常量:
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所沒有的。
import ( "io"
"log""strconv")
類型轉換go對於變量的類型有嚴格的限制,不同類型之間的變量不能進行賦值、表達式等操作,必須要要轉換成同一類型才可以,比如int32和int64兩種int類型的變量不能直接相加,要轉換成一樣才可以。
var a int32 = 13
var b int64 = 20
c := int64(a) + b
這種限制主要是防止我們誤操作,導致一些莫名其妙的問題。在java中因為有自動轉型的概念,所以可以不同類型的可以進行操作,比如int可以和double相加,int類型可以通過+和字符串拼接起來,這些在go中都是不可行的。
mapmap類型,Java裡是Map接口,go裡叫做字典,因為其常用,在go中,被優化為一個語言上支持的結構,原生支持,就像一個關鍵字一樣,而不是java裡的要使用內置的sdk集合庫,比如HashMap等。
ages := make(map[string]int)
ages["linday"] = 20
ages["michael"] = 30
fmt.Print(ages["michael"])
go裡要創建一個map對應,需要使用關鍵字make,然後就可以對這個map進行操作。
map的結構也非常簡單,符合KV模型,定義為map[key]value, 方括號裡是key的類型,方括號外緊跟著對應的value的類型,這些明顯和Java的Map接口不同。如果在go中我們要刪除map中的一個元素怎麼辦?使用內置的delete函數就可以,如下代碼刪除ages這個map中,key為michael的元素。
delete(ages,"michael")
如果我們想遍歷map中的KV值怎麼辦?答案是使用range風格的for循環,可比Java Map的遍歷簡潔多了。
for name,age := range ages {
fmt.Println("name:",name,",age:",age)
}
range一個map,會返回兩個值,第一個是key,第二個是value,這個也是go多值返回的優勢,下面會講。
函數方法在go中,函數和方法是不一樣的,我們一般稱包級別的(直接可以通過包調用的)稱之為函數,比如fmt.Println();把和一個類型關聯起來的函數稱之為方法,如下示例:
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,如果只能返回一個值,我們就需要調用兩次方法才可以。
func GetTime() (time.Time,error){ return time.Now(),nil}
多值返回也很簡單,返回的值使用逗號隔開即可。如果要接受多值的返回,也需要以逗號分隔的變量,有幾個返回值,就需要幾個變量,比如這裡:
now,err:=GetTime()
如果有個返回值,我們用不到,不想浪費一個變量接收怎麼辦?這時候可以使用空標誌符_,這是java沒有的。
now,_:=GetTime()
指針Go的指針和C中的聲明定義是一樣的,其作用類似於Java引用變量效果。
var age int = 10
var p *int = &age
*p = 11
fmt.Println(age)
其中指針p指向變量age的內存地址,如果修改*p的值,那麼變量age的值也同時會被修改,例子中列印出來的值為11,而不是10.
相對應java引用類型的變量,可以理解為一個HashMap類型的變量,這個變量傳遞給一個方法,在該方法裡對HashMap修改,刪除,就會影響原來的HashMap。引用變量集合類最容易理解,自己的類也可以,不過基本類型不行,基本類型不是引用類型的,他們在方法傳參的時候,是拷貝的值。
結構體替代類Go中沒有類型的概念,只有結構體,這個和C是一樣的。
type Person struct {
age int
name string}
Go中的結構體是不能定義方法的,只能是變量,這點和Java不一樣的,如果要訪問結構體內的成員變量,通過.操作符即可。
func (p Person) GetName() string { return p.name
}這就是通過.操作符訪問變量的方式,同時它也是一個為結構體定義方法的例子,和函數不一樣的是,在func關鍵字後要執行該方法的接收者,這個方法就是屬於這個接收者,例子中是Person這個結構體。
在Go中如果想像Java一樣,讓一個結構體繼承另外一個結構體怎麼辦?也有辦法,不過在Go中稱之為組合或者嵌入。
type Person struct {
age int
name string
Address
}type Address struct {
city string}
結構體Address被嵌入了Person中,這樣Person就擁有了Address的變量和方法,就像自己的一樣,這就是組合的威力。通過這種方式,我們可以把簡單的對象組合成複雜的對象,並且他們之間沒有強約束關係,Go倡導的是組合,而不是繼承、多態。
接口Go的接口和Java類型,不過它不需要強制實現,在Go中,如果你這個類型(基本類型,結構體等都可以)擁有了接口的所有方法,那麼就默認為這個類型實現了這個接口,是隱式的,不需要和java一樣,強制使用implement強制實現。
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語言的線程,不過是不一樣的。
go run()
這就啟動一個goroutine來執行run函數,代碼非常簡潔,如果在java中,需要先New一個Thread,然後在重寫他的run方法,然後在start才可以開始。
兩個goroutine可以通過channel來通信,channel是一個特殊的類型,也是go語言級別上的支持,他類似於一個管道,可以存儲信息,也可以從中讀取信息。
package mainimport "fmt"func main() {
result:=make(chan int) go func() {
sum:=0以上示例使用一個單獨的goroutine求和,當得到結果時,存放在result這個chan裡,然後供main goroutine讀取出來。當result沒有被存儲值的時候,讀取result是阻塞的,所以會等到結果返回,協同工作,通過chan通信。
對於並發,go還提供了一套同步機制,都在sync包裡,有鎖,有一些常用的工具函數等,和java的concurrent框架差不多。
異常機制相比java的Exception來說,go有兩種機制,不過最常用的還是error錯誤類型,panic只用於嚴重的錯誤。
type error interface {
Error() string}
go內置的error類型非常簡潔,只用實現Error方法即可,可以列印一些詳細的錯誤信息,比如常見的函數多值返回,最後一個返回值經常是error,用於傳遞一些錯誤問題,這種方式要比java throw Exception的方法更優雅。
Defer代替finallygo中沒有java的finally了,那麼如果我們要關閉一些一些連接,文件流等怎麼辦呢,為此go為我們提供了defer關鍵字,這樣就可以保證永遠被執行到,也就不怕關閉不了連接了。
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名了,上升速度還是非常快的,而且隨著服務化,容器化,他的優勢會越來越多的顯現出來,得到更廣泛的應用。
如果你感興趣,那麼開始吧,提前準備,機會來的時候,就不會錯過了。
推薦閱讀
喜歡本文的朋友,歡迎關注「Go語言中文網」: