自Google在2009年發布Go語言的第一個正式版之後,這門語言就以出色的語言特性受到大家的追捧,尤其是在需要高並發的情境下,大家都會想到是不是該用Go。隨後,在國內湧現出了一批以七牛為代表的使用Go作為主要語言的團隊,而許世偉大神本人也在各種場合下極力推動Go在國內的發展,於是在這種大環境下,中國的Go開發人員群體逐漸超越了其他地區。
那麼問題來了,業餘時間好學是一回事,真正要將一個新東西運用到生產中則是另一回事。JavaScript的開發人員可以義無反顧地選擇Node.js,但是對於Java開發人員來說,在下一個大項目裡究竟是該選擇Go,還是Java呢?
鄭重聲明:本文並不是來探討Go或者Java誰是更好的語言,每種語言都有自己的設計哲學和適用情境,今天主要是在探討實際工程中的選擇和權衡的問題,所以請不要上綱上線。
語言本身
首先,需要說明一下,作為一個技術決策者,在進行技術選型時並不能單方面地根據語言本身的特點直接下結論。實際情況下,大多數人會使用一系列的架構、庫及工具,簡而言之就是會考慮很多周邊生態環境的因素,同時還要結合公司的特點、各種曆史問題和實際客觀因素等等一系列的考慮點綜合下來才能完成決策。所以,接下來我們先從語言開始,一步一步來分析下在你的項目中選擇Go是否合適。
Go在高並發編程方面無疑是出眾的,通過goroutine從語言層面支援了協程,這是Java等語言所無法比擬的,這也是大多數人在面對高並發情境選擇Go的重要原因之一。雖然Java有Kilim之類的架構,但沒有語言層的支援始終稍遜一籌。
除此之外,Go的其他文法也很有趣,比如多傳回值,在一定程度上為開發人員帶來了一定的便利性。試想,為了返回兩到三個值,不得不封裝一個對象,或者抹去業務名稱使用Map、List等集合類,進階一點用Apache的Pair和Triple,雖然可行,但始終不如Go的實現來得優雅。在此之上,Go也統一了異常的返回方式,不用再去糾結是通過拋異常還是錯誤碼來判斷是否成功,多傳回值的最後一個是Error就行了。
Go在語言的原生類型中支援了常用的一些結構,比如map和slice,而其他語言中它們更多是存在於庫中,這也體現了這門語言是從實踐角度出發的特點,既然人人都需要,為什麼不在語言層面支援它呢。函數作為一等公民出現在了Go語言裡,不過Java在最近的Java 8中也有了Lambda運算式,也算是有進步了。
其他的一些特性,則屬於錦上添花型的,比如不定參數,早在2004年的Java 1.5中就對varargs有支援了;多重賦值在Ruby中也有出現,但除了多傳回值賦值,以及讓你在變數交換值時少寫一個中間變數,讓代碼更美觀一些之外,其他的作用著實不是怎麼明顯。
說了這麼多Go的優點,當然它也有一些問題,比如GC,說到它,Java不得不露出潔白的牙齒,雖然在大堆GC上G1還有些不盡如人意,但Java的GC已經發展了很多年,各種策略也比較成熟,CMS或G1足以應付大多數情境,實在有要求還能用Azul Zing JVM。不過從最新的Go 1.5的訊息來看,Go的GC實現有了很大地提升,順便一提的是GOMAXPROCS預設也從1變成了CPU核心數,看來官方對Go在多核的利用方面更有信心了。
許世偉在《Go 語言編程》的前言中預言未來10年,Go會取代Java,位居編程榜之首,當時是2012年,為了看看2009年TIOBE年度程式設計語言如今的排名,筆者在撰寫本文時特意去TIOBE看了下,最近的2015年8月熱門排行榜,Java以19.274%位居榜首,Go已經跌出了前50,這不禁讓人有些意外。
但總體上來說,筆者認為Go在語言層面的表現還是相當出色的,解決了一些編程中的痛點,學習曲線也能夠接受,特別是對於那些有C/C++背景的人,會感覺十分親切。
工程問題
一個人寫代碼時可以很隨性,想怎麼寫就怎麼寫,但當一個人變成一個團隊後,這種隨性或者說隨便就會帶來很多問題,於是就誕生了編碼規範這玩意兒,大廠基本都有自己的編碼規範,比如Google就有針對不下十種程式設計語言的規範。團隊內約定一套編碼規範能夠很大程度上地確保代碼的風格,降低閱讀溝通的成本。Go內建了一套編碼規範,違反了該規範代碼就無法編譯通過,可以說只要你是寫Go的,那你的代碼就不會太難看,當然Go也沒有把所有東西就強制死,還有一些推薦的規範可以通過gofmt進行格式化,但這步不是必須的。
雖然Go自己解決了這個問題,但並不能說Java在這方面是空白,Java發展至今周邊工具無數,並不缺成熟的代碼靜態分析工具,比如CheckStyle、PMD和FindBugs,它們不僅能掃描編碼規範的問題,甚至還能掃描碼中潛在的問題並給出解決方案,並且使用方便,在Java開發人員社區中有很高地接受度,應該說大多數靠譜地開發人員都會使用這些工具。除此之外,一些大廠也有自己的強制手段,比如百度內部也有很多語言的編碼規範,而且大部分情況下如果沒有通過編碼規範的掃描,你是無法提交代碼的;還有一些公司會在持續整合過程中加入代碼掃描,有FindBugs高優先順序的問題時必須修複才能進入下一個階段。所以說Go在這個問題上的優勢並不明顯,或者說在一個成熟的環境下,這隻是合格而已。
這裡需要強調筆者的一個觀點:
Go在語言本身和發行包中融入了很多最佳實務,正是這些前人的經驗才讓它看起來如此優秀。拿這麼個海陸空混編特種部隊去和Java、C、Ruby這些語言本身做對比,顯得不太公平,所以本文在考慮問題時都會結合語言及其生態圈中的成員,畢竟這才更接近真實的情況。
Go本身對項目結構有一套約定,代碼放哪裡,測試檔案如何命名,編譯打包後的結果輸出到哪個目錄,甚至還有go cover這種統計測試覆蓋率的命令列,開發人員不用在這些問題上太過糾結,再一次體現了Go注重工程實踐的特點。回過頭來,Java方面,Maven、Gradle都是注重於工程生命週期管理的工具,而且Maven更是曆史悠久,被廣泛用於各種項目之中。以Maven為例,不僅能夠實現上述所有功能,還有很強的外掛程式擴充能力,這裡需要的只是一次性維護好pom.xml檔案就行了,由於Maven的使用群很大,網上有大量的範例,甚至還有很多產生工程的工具和模板,所以使用成本並不高。
這裡還要衍生出一個話題,就是依賴管理,在開發代碼時,勢必需要依賴很多外部的東西,Go可以直接import遠端內容,這個特性很有創意,但並不能很好地解決版本的問題,在Maven或Gradle裡,我們可以直接指定各個依賴項甚至是外掛程式的版本,工具會自動從倉庫中下載它們。如果需要同時在同一個系統的不同模組裡依賴同一個庫的不同版本,我們還能夠通過OSGi這種略顯複雜的手段來實現,在模組化方面,Jagsaw雖然被一延再延,但估計有望納入Java 9,這個特性也會解決不少問題。而根據Golang實踐群中大家的討論,似乎godep、gb和gvt都不盡如人意,在這點上看來Go還有一段路要走。
綜上所述,Go在工程方面的確有不少亮點,吸納了很多最佳實務,甚至可以說用Go之後更容易寫出規範的代碼,有好的項目結構,但與生態圈完備的Java相比,Go並不佔優勢,因為最終代碼的品質還是由人決定的,雙方都不缺好的工具,所以這方面的特點並不能影響技術選型的決策。
開發實踐
Talk is cheap. Show me the code.
下面進入編碼環節,先從Go引以為傲的並發開始,《Go語言編程》的前言中有這樣一段代碼:
func run(arg string){// ...}funcmain(){go run("test")...}
書中與之對比的Java代碼有12行,而且還是線程,不是協程,對比很明顯,但那是在2012年的時候,時至今日,Java已經發展到了Java 8,3年了,看看如今的Java代碼會是什麼樣的:
public class ThreadDemo{publicstaticvoidmain
(String[]args){Stringstr="test";
// 為了和原先的Java版本對照,說明能傳參進入線程內,
在外聲明了一個字串,其實可以直接寫在Lambda裡
new Thread(()->
{/* do sth. with str */}).start();}}
不是協程仍是硬傷,但有了Lambda運算式,代碼短了不少。不過話又說回來,這樣的比較並沒有太多意義,所以各位Go粉也不用站出來說Go也支援閉包,Go的版本也能精簡。我們比的不是誰寫的短,在Java實踐中,大多數時候大家會選擇線程池,而不是自己new一個Thread對象,Doug Lea大神的Java並發包非常的好用,而且很靠譜。另外,並發中處理的內容才是關鍵,新啟一個線程或者協程才是萬裡長城的第一步,如果其中的商務邏輯有10個分支,還要多次訪問資料庫並調用遠程服務,那無論用什麼語言都白搭。所以在商務邏輯複雜的情況下,語言的差異並不會太明顯,至少在Java和Go的對比下不明顯,至於其他更高階、表達力更強的語言(比如Common Lisp),大家就要拼智商了。
還有一些情況中,由於客觀因素制約,完全就無法使用Go,比如現在如火如荼的互連網金融系統裡,與銀行對接的系統幾乎沒有選擇,都是Java實現的,因為有的銀行只會給Jar包啊……給Jar包啊……Jar包啊……如果是個so檔案,也許還能用cgo應付一下,面對一個Jar你讓Go該何去何從?
拋開這些讓人心煩的問題,讓我們再來看看現在比較常見的如何?REST服務。說到這裡,就一定要祭出國人出品的Beego架構。一個最簡單的REST服務可以是這樣的:
packagemainimport("github.com/astaxie/beego")
typeMainController struct
{ beego.Controller}func
(this*MainController)Get()
{ this.Ctx.WriteString
("hello world!")}
func main()
{ beego.Router("/",
&MainController{}
)beego.Run()}
既然Go方面,我們使用了一套架構,那麼Java方面,我們一樣也選擇一個成熟的架構,Spring在Java EE方面基本可以算是事實標準,而Spring Boot更是大大提升了Spring項目的開發效率,看看同樣實現一個REST服務,在SpringBoot裡是怎麼做的。
首先,到start.spring.io根據需要產生項目骨架(其實完全可以方便地自己通過Maven手工配置依賴或者是用CLI工具來建立),為了後續的示範,這裡我會選上“Web”、“Actuator”和“Remote Shell”,其實就是多了兩個Maven的依賴,下文營運部分會提到,然後隨便找個順手的IDE開啟工程,敲入如下代碼就行了(import、包和類定義的部分基本都是IDE產生的)。
packagedemo;
import org.springframework.boot.
SpringApplication;import org.
springframework.boot.autoconfigure.
SpringBootApplication;import org.
springframework.web.bind.annotation.
RequestMapping;import org.
springframework.web.bind.annotation.
RestController;@SpringBootApplication@RestControllerpublicclassDemoApplication
{ @RequestMapping("/")
public String sayHello()
{ return "hello world!"; }
public static void main(String[]args)
{ SpringApplication.run
(DemoApplication.class, args); }
}
運行這段代碼會自動啟動內建Tomcat容器,訪問http://localhost:8080/就能看到輸出了。因為其實就是Spring,所以可以毫無壓力地與其他各種架構設施組合,也沒有太多學習成本。
可見兩者在實現REST服務方面,並沒有太大的差別,加之上文提到的商務邏輯問題,只要運用恰當的工具,兩種語言之間並不會產生質的差異。
Beego中的ORM支援MySQL、PostgreSQL和Sqlite3,而在Java裡Hibernate和myBatis這樣的ORM工具幾乎能通吃大多數常見的關係型資料庫,且相當成熟,社區配備了各種自動產生工具來簡化使用,行業裡還有JPA這樣的公認標準。縱觀Go的ORM工具,大家還是在探討,究竟哪個才好用呢?切到NoSQL方面,雙方都有大量的驅動可以使用,比如MongoDB和Redis都有詳盡的驅動列表,MongoDB還沒有官方驅動,但有社區維護的mgo,算是打成平手吧。再大一點,像用到Hadoop、Spark和Storm的情境下,似乎Java的出鏡率更高,或者是直接通過Streaming方式就解決了,此處也就不再展開了。
雖然說了這麼多問題,但如果真的遇到了大流量、高並發的情境,需要從頭開始開發用來處理這些問題的基礎設施時,Go還是不錯的選擇。比如,七牛這樣的雲端服務供應商,又或者是BFE(Baidu Front End,號稱可能是全世界流量最大的Go語言叢集 ,在2015年的Velocity大會上留下了它的身影——圖1和圖2)這樣的硬貨,請不要糾結。
營運
寫完代碼只是萬裡長征的一小步,後面還有一大堆的事情等著你去解決,比如怎麼把寫完的代碼編譯、打包、發布上線。編譯打包就不說了,Go的命令列工具go build就能直接把你的代碼連同它的所有依賴一起打成一個可執行檔。至於部署,大家都稱讚Go的部署沒有依賴(除了對glibc的版本有要求,不考慮需要cgo的情況),直接把可執行檔往那裡一扔就好了,非常方便。Go內建了強大的HTTP支援,不需要其他Web伺服器來做支撐就能獲得不錯的效能。
再來看看Java,按照常理,一般都會使用Maven或者Gradle來處理編譯、打包,甚至是發布,仍舊以Maven為例,mvn package就能完成編譯和打包。可以選擇Jar包,如果是Web項目部署到容器裡的話可以是War包,也可以將各種資源打包到一起放到壓縮包(zip、tar等等)裡,這個步驟並不複雜。
接下來的部署環節,大家就有話要說了,“Write Once, Run Anywhere”這曾是Java的宣傳語,但正是這句話一直被大家詬病,其實如果代碼中不使用平台特定的內容(比如避免綁定在WebLogic上),不使用某個特定版本JDK的內部類(比如com.sun裡的東西,這種做法本來就不推薦),Java的代碼還是能夠做到編譯後在任何地方都能啟動並執行,事實上現在絕大部分情況下,大家也都是這麼做的,看看廣大的Java庫都是發布Jar到Maven倉庫的,也沒誰讓你直接拉源碼來編譯。在不同的環境下,只需要部署了對應的JDK就好了(一般放到裝機模板裡,或者直接拿安裝包部署一下就好了),至於是什麼作業系統其實並不重要。
延續上文REST服務的例子,Java的Web項目一般都會部署到容器裡,比如Tomcat或者Jetty,當然也有用商業容器的(很多銀行就是用的WebLogic),所以大家就都認為部署Java程式需要先有容器,這其實是幾年前的事情了,後來颳起了一股內嵌容器的風潮,Tomcat和Jetty都可以嵌入到你的程式裡,再也不用為有沒有容器而煩惱了。Spring Boot索性把這件事變得更簡單了,mvn package後,一句話就能搞定內建Tomcat的啟動、完成各種部署,然後一切就變成下面這樣(假設最後產生的Jar包名為demo.jar):
java-jar demo.jar
在Spring Boot 1.3裡,還能通過調整Maven Plugin的配置,讓Jar可以直接執行(不要小看這麼一個變化,它可以大大提升可營運性):
./demo.jar
所以說Java程式難部署其實也是曆史,現在的Java程式部署早已是另一番光景。兩者的編譯、打包、部署環節完全可以打成平手。筆者認為有些方面Java反而更勝一籌,比如Java基本就不用操心交叉編譯的問題;Go的庫在發布時推薦直接發布源碼而非二進位包,遇到天朝特有的網路無法訪問的情況,編譯個東西還要自備梯子……至於和Nginx等等的配合,更是大家都很方便,就不再贅述了。
完成了部署,接下來的日誌和監控,都是很常規的問題,日誌各自有對應的庫,而監控都是依賴專業的監控平台,自己做好資訊輸出就好了,請容我再秀一下Spring Boot的RemoteShell終端監控,除了常規的HTTP方式輸出JSON資訊(內建了健全狀態檢查、儀錶資料、Dump、請求跟蹤等一系列REST輸出),還內建了這麼個類似top的高大上的玩意兒,ssh -p 2000 user@localhost後執行dashboard可以看到這個即時更新的介面。
總結
說了這麼多,來總結下全文的觀點——雖然Go在語言上表現的很出色,也融入了很多最佳實務,但是結合多方考慮,在很多情況下它並不會比Java帶來更多價值,甚至還不一定能做的比Java好,因此作為一個Java程式員,我不會在自己的生產項目中轉向Go。
此外,除了本文重點討論的那些問題,還有更現實的問題擺在那裡,比如團隊轉型成本和招聘的成本,千萬不要小看招聘,對於管理者而言,招聘也是工作中的重要內容,試想一下,是招個有經驗的Go程式員容易,還是招一個有經驗的Java程式員容易,就算能招到一個會Go的正式員工,你能招到一個會Go的外包麼,特別是在團隊急需補充新鮮血液時,結果是顯而易見的。
但這一切都不妨礙大家來學習Go,本文開頭就已經表達過這一觀點,業餘時間學習Go和在生產項目中不用Go並不衝突,Go還是有很多值得學習和借鑒的地方,而且誰也說不準哪天你就真遇上了適合用Go的項目呢。