導讀:對於軟體開發的程式語言,其實沒有萬能靈藥。
本文作者詳細介紹了他使用Java和Go這兩種程式語言,一個是傳統語言,一個是新興語言的工作方式。
Go VS Java
實話說,我很喜歡Java這門語言。近幾年來,我在公司裡積累了大量關於EJB2,DB2與Oracle等後端開發的專業知識。
現在我轉向到基於自然語言處理的開發方向,如Spring Boot、Redis、RabbitMQ、Open NLP還有UIMA等技術。
因此說來,我選擇的語言是Java,這個語言一直有旺盛的生命力,寫起程序來也很有意思。
開始測試Go!
在2017年初,我接到了一個很有意思的項目。這個項目圍繞著自動化系統來監控農作物和花花草草等植物們。
原先的開發團隊原始碼部署在三個不一樣的系統中:Windows,MacOS和ARM,他們使用Go語言做為網關。
坦白講,當時我對Go語言一點兒也不熟悉,隨著項目的發展,我的工作內容包括對新技術的學習以及產品實施。項目現有代碼庫結構複雜,我面臨的挑戰比較大。使用Go寫的針對於物聯網平臺支持程序,要在三個異構系統上部署、測試與運維。這個系統使用單例模式編寫,系統耦合嚴重,模塊之間相互雜揉,可以說是一處錯誤,多處崩潰。我用Java的開發風格重新構建了網關的新版本,但後讓我弄的比原來更醜陋、混亂了。
我後來升職為一家公司任技術總監。
我試著不再使用Java的方式來開發Go了,儘量更多的使用Go的語言特性來做,全面擁抱該語言。
如此下來我才真正發現,Go的確是一個創新且全面的程式語言,我的團隊開始將它用它來開發各種各樣的項目。
不容置疑的是,與任何程式語言一樣,Go有優點也有不少缺點,我這個人不太會撒謊,有時候我還蠻想念Java的。
依照以往我的編程經驗,軟體開發方面沒有靈丹妙藥,程式語言就是典型。以下我來講一下心得體會,使用一門傳統語言和創新語言新手是怎麼幹活的。
Go與Java的相似之處
Go語言與Java都從屬於C語言家族,既然是遠房親戚,它們有著相似的語法。所以,Java開發者閱讀Go寫的代碼並不會太難,反之亦然。
Go語句在語句末尾並不使用;分號結束。不用擔心,除了極少數情況,我閱讀Go寫的代碼反倒感覺更清晰易讀。
Go與Java都使用我最喜歡的功能之一,那就是垃圾收集器(GC),以防止內存洩漏。
C/C++程式設計師都知道,自己在寫程序時要注意避免內存洩漏,垃圾回收機制讓內存管理自動化,從而簡化程式設計師在代碼中處理類似的功能。
Go語言的GC工具的表現很優秀,它的處理時間在1.8版本中不到1ms即可完成。
在Go語言中,其GC的設置參數並不多,有一個唯一的GC變量用來設置初始時垃圾回收目標的百分比。在Java中,有4個不同的垃圾收集器,每一個都要有大量的設置工作。
Java和Go都被視為跨平臺語言,但是Java需要Java虛擬機(JVM)來解析編譯後的代碼,而Go可以簡單地將代碼編譯為給定平臺的二進位文件。
Java與Go相比,Go比Java依賴度更低,因為Go每次為一個平臺編譯代碼時都要創建一個二進位文件。從測試和DevOps角度來看,分別編譯不同平臺的二進位文件非常的費時,有時候使用GO組件時,跨平臺的Go編譯在一些情況下並不起作用。但是,使用Java則可以在有JVM的任何地言使用一個Jar。
Go佔用內存很小,而且並不需要安裝和管理虛擬機的關聯依賴,以及複雜的注意事項。
接下來談一下反射功能。Java的反射功能很強大,而Go的反射更偏複雜。Java是一種純面向對象語言,所有內容都被視為對象。
在Java使用反射,需要為對象創建一個類,並從此類中獲取所需要的信息。如下代碼所示:
Class cls = obj.getClass();
Constructor constructor = cls.getConstructor();
Method[] methods = cls.getDeclaredFields();接下來就可以訪問構造函數、方法和屬性,然後去調用和設置它們。
在Go語言中,則沒有類的概念,它的結構中僅包含已聲明的欄位。因此,我們需要它的「反射」包來提供相關信息。
type Foo struct {
A int `tag1:"First Tag"
tag2:"Second Tag"`
B string
}
f := Foo{A: 10, B: "Salutations"}
fType := reflect.TypeOf(f)
switch t.Kind(fType)
case reflect.Struct:
for i := 0; i < t.NumField(); i++ {
f := t.Field(i)
// ...
}
}我認為,這並不是一個大問題,因為Go中沒有用於結構的構造函數,因此結果是許多原始類型,且必須分別處理,還需要認真考慮指針。
在Go中,我們還可以通過指針或值傳遞某些東西。Go語言結構具有欄位的功能,而不是方法。
再來說一下兩個語言的輔助功能。
Java具有Private、Protected以及Public這3個修飾符,用來對數據、方法和對象提供不同範圍的存取。
Go語言中,有著和Java類似的語句,它們是exported/unexported修飾符。在Go中如果沒有修飾符,以大寫字母開頭的所有內容都能夠導出,並且在其它軟體包中可見。unexported,以小寫變量或函數僅在當前包中可見。
Go與Java大方面的差異
Go語言並不是一種面向對象(OOP)語言。它沒有像Java中的繼承,沒有通過繼承這一特性實現系統的多態性。
Go語言沒有對象,只有結構體。但它可以通過提供接口實現一些面向對象的模式。同樣道理,可以將將結構彼此嵌入,但是嵌入式結構無法訪問宿主結構的數據和方法。
Go中使用組合,而不使用繼承來組合一些所需的行為和數據。
Go是一種命令式語言,而Java則是一種聲明式語言。
Go中沒有像依賴式注入那樣的東西,在Go中必須將所有內容明確的包裝在一塊。因此,在Go中編程末儘量少用一些魔術方法,一切都是可見的。
Go程式設計師要了解Go代碼如何使用內存、文件系統以及其它關聯資源的全部機制。
從另一方面,Java需要開發人員更多的關注自定義開發的業務邏輯部分,諸如如何創建、過濾、更改和存儲數據。
從系統基礎架構和資料庫管理系統而言,所有這些都是通過配置和通過Spring Boot等通用框架來完成。
人們總是矛盾心理,我們對基礎架構的部分感到枯燥乏味,因此這部分功能交給框架。這會給我們帶來方便,但對於程式設計師來說,控制權在框架,也限制人們對整個流程優化的能力。
再討論一下兩個語言的變量定義,以及順序。在Java中定義變量類似於以下:
在Go語言,這樣來寫:
我在第一次使用Go時,沒有;號的代碼讓我有些困惑,就像寫文章沒寫句號一樣,感覺沒有寫完。
使用Go語言的優勢
Go有著簡單且且優雅的並發能力。Go語言有著強大的並發模型,稱之為「通信順序過程」或CSP。在Go中使用n-to-m嗅探器,該嗅探器允許n個系統線程中發生m個並發執行。
可以利用Go語言的關鍵字:go(與該語言的名字相同)啟動例程。比如下面的字符串:
此時函數doMyWork()將同時開始執行。
在Go中進程之間的通信可以通過共享內存(並不推薦)和共享通道來完成。它允許開發者使用GoMaxProcs環境變量定義一個多內核的健壯且流暢有並發系統。
Go提供了一種特殊模式-race(競賽)來運行二進位文件,並同時檢查運行情況。通過此機制來證明軟體是並發安全的。
以上命令格式,即以競賽檢測模式運行一個應用程式。
我個人很喜歡Go提供的即開即用功能(golang.org/dl),它提供一個很好的例子,如sync同步功能(golang.org/pkg/sync)包。
比如針對實現Singleton模式實現,可以如下代碼表示:
package singleton import ("sync")
type singleton struct { }
var instance *singleton
var once sync.Once
func GetInstance() *singleton {
once.Do(func() {
instance = &singleton{}
})
return instance
}同步包還為並發實現映射、互斥鎖、條件變量和等待提供了一種結構。它的另一個包atomic(golang.org/pkg/sync/atomic)提供了並發安全轉換與數學運算。
實際上,Go提供了並發代碼所需要的全部。
Duck typing
「如果你說話像鴨子,走路像鴨子,那它一定就是只鴨子」。這句話在Go的世界裡是正確的。在Go語言中,不需要定義某種接口,直接實現結構即可。在Java中,對象比較顯式聲明並實現了接口。
探查器。Go提供的性能分析工具,使用非常方便、快捷和容易。Go中的事件探查器有助於顯示程序中的內存分配和CPU使用情況,還可以在可視化圖形中加以說明,這對優化應用程式特別有用。
Java可以從Java VisualVM這些探查器中來開始,但它們不像Go的探查器那樣簡單,而且這些工具的功效取決於JVM,其獲取的統計信息和垃圾收集與JVM的具體工作有關。
C 與Go
Go中允許對C語言進行簡單且強大的集成。開發者可以在Go項目中編寫有C語言代碼的平臺相關應用。從底層來講,CGo可以讓開發人員創始調用C代碼的Go程序包。為了排除/包含指定平臺的C代碼段,如各種構建器選項,這些代碼段可以實現應用程式的多平臺化。
函數作為參數使用
Go函數可以用作變量,傳遞給另一個函數或用作結構的欄位。Go語言的這種多功能性的確讓人耳目一新。
而Java是從1.8開始結合了lambda的使用,但它並不是真正的函數,而是一個單功能的對象。它的這一特性就是想實現Go中使用函數的行為,而Go在一開始就
存在於語言特性中。
明晰的代碼風格。在Go語言社區中,有著眾多熱情和能力強大的支持者,社區中有大量對Go的使用示例和操作的最佳實踐和方法。
其地址為https://golang.org/doc/effective_go.html
在Go的函數中可以返回多個參數,這是一個非常有用和優秀的方式。如下代碼:
package main
importt "fmt"
func returnMany() (int,string,error){
return 1,"example",nil
}
func main(){
i,s,err :=returnMany()
fmt.Printf("Returned %s %s %v",i,s,err)
}Go語言的缺點
Go除了接口外,沒有多態特性。Go中沒有多態性,這表示如果在同一個程序包中,有兩個函數具有不同的參數,但是含義相同,這時必須給它們修改為不同的名稱。請看如下之代碼:
func makeWorkIntt(number int){
fmt.Printf("Work done number %d",number)
}
func makeWorkStr(title string){
fmt.Printf("Work done title %s",title)
}其實這些方法都做同樣的事情,只是方法名稱不同,而且代碼比較醜陋。
此外,在Go中沒有繼承的多態性。如果嵌入到結構,則嵌入式結構只知道自己的方法,而不會知道宿主結構的方法。這對於一些有其它語言經驗開發者有一定的挑戰性,他們在之前使用的OOP語言主要使用繼承處理,當使用GO語言時會有一些困惑。
隨著時間的流轉,我自己開始意識到繼承式的多態化處理只是一種思維方式,結構式處理很可靠,也更明顯,且運行時間可變。
關於錯誤處理,處理什麼返回錯誤信息,以及如何返回錯誤。作為開發者,我們需要每次都返回錯誤並傳遞相關錯誤。有時候錯誤會隱藏,這確實是麻煩的所在,要記住檢查錯誤並把它們傳遞出去。
在Java中異常處理要方便得多,比如RuntimeException,都可以不加入函數的籤名中即可使用。
public void causeNullPointerException() {
throw new NullPointerException("demo");
}
/*
...
*/
try {
causeNullPointerException();
} catch(NullPointerException e) {
System.out.println("Caught inside fun().");
throw e; // rethrowing the exception
}此外,Go語言沒有泛型支持,雖然這一特性會增加複雜性,Go的創建者也認為代價高昂。在Go語言創建時,必須針對不同的類型重複使用自己的或生成代碼。
沒有注釋
在Go中可以用代碼生成部分注釋,但是不幸的是,在運行時注釋根本不能替換,這是有原因的——Go不是聲明性的,且代碼中不應包含任何魔術。
我喜歡在Java中使用注釋,它們讓代碼更加優雅、簡單、簡約。這些注釋可以生成大量文件,在做HTTP結點提供某些元數據時非常有用。在GO語言中,必須手動或直接製作Swagger文件,或作為結點功能提供特殊注釋。Java中的注釋是一種魔術方式,人們通常不必關心它們的工作方式。
再來談一下Go中的依賴管理。在本文之前我曾寫過一篇GO依賴項管理的文章。Go依賴管理環境在相當長一段時間中存在一些麻煩。最初,除了「Gopgk」外沒有任何依賴項管理,後來又發布「Vendor」後被「vgo」取代。現在可以手動和使用go命令(如go get命令)來修改go.mod文件描述符來處理。
不幸的是,這種修改會引發依賴關係不穩定。
Go沒有開箱即用的源鏡像依賴關係管理機制,而Java有著諸如Maven和Gradle之類著名的依賴管理聲明類工具。它們也可能用來構建,部署和處理其它集成用途。
我們在實際開發中使用Makefile、Docker-composer和bash腳本自定義構建所需的依賴關係管理,會讓持續交付和集成的過程、穩定性變複雜。
在Go中,引用軟體包的名稱要包含託管的GitHub域名。例如:
import 「github.com/pkg/errors」這塊實在有點奇怪,而且非常不方便,如果不更改整個項目代碼庫的導入,就無法用自己的實現替換其它人的代碼。
而在Java中,導入通常以公司名字開頭。像這樣:
import by.spirascout.public.examples.simple.Helper;當然,我想在GO中的這些依賴管理問題都只是暫時的,將來會以最有效的方式解決。
總結
Go語言中最有趣的一塊是,它遵循一種代碼命名準則,稱為代碼可讀性方法的心理學。使用Go語言,可以使用單獨的方法來構建清晰且可維護的代碼,雖然看起來像是一些單詞組合在一起,但實際上清晰可讀。
一些使用Go進行Web開發的公司,它們將項目展示給我,代碼體現了Go的快速,強大且易於理解,非常適合小型服務和並發處理。而對大型,複雜的系統,比如功能更複雜的服務以及微服務系統,Java暫時保持在世界頂級程式語言的地位。
儘管Java SE已經成為Oracle的收費商品,而Go則是技術社區的孩子。已經產生多個品牌的JVM,而Go的工具集則是相同的。
可以確定的是,不同的任務需要不同的工具,語言也是如此。
關聯閱讀:
可能是世界上最簡單的用 Go 來寫 WebAssembly 的教程
Golang與Python,哪種程式語言更適合您?
PHP VS Golang,是一個艱難選擇嗎?