自Google在2009年發(fā)布Go語言的第一個正式版之后厚满,這門語言就以出色的語言特性受到大家的追捧府瞄,尤其是在需要高并發(fā)的場景下碧磅,大家都會想到是不是該用Go碘箍。隨后,在國內(nèi)涌現(xiàn)出了一批以七牛為代表的使用Go作為主要語言的團(tuán)隊(duì)鲸郊,而許世偉大神本人也在各種場合下極力推動Go在國內(nèi)的發(fā)展丰榴,于是在這種大環(huán)境下,中國的Go開發(fā)者群體逐漸超越了其他地區(qū)秆撮。
那么問題來了四濒,業(yè)余時間好學(xué)是一回事,真正要將一個新東西運(yùn)用到生產(chǎn)中則是另一回事职辨。JavaScript的開發(fā)者可以義無反顧地選擇Node.js盗蟆,但是對于Java開發(fā)者來說,在下一個大項(xiàng)目里究竟是該選擇Go舒裤,還是Java呢喳资?
鄭重聲明:本文并不是來探討Go或者Java誰是更好的語言,每種語言都有自己的設(shè)計哲學(xué)和適用場景腾供,今天主要是在探討實(shí)際工程中的選擇和權(quán)衡的問題仆邓,所以請不要上綱上線。
語言本身
首先伴鳖,需要說明一下节值,作為一個技術(shù)決策者,在進(jìn)行技術(shù)選型時并不能單方面地根據(jù)語言本身的特點(diǎn)直接下結(jié)論榜聂。實(shí)際情況下搞疗,大多數(shù)人會使用一系列的框架、庫及工具峻汉,簡而言之就是會考慮很多周邊生態(tài)環(huán)境的因素贴汪,同時還要結(jié)合公司的特點(diǎn)、各種歷史問題和實(shí)際客觀因素等等一系列的考慮點(diǎn)綜合下來才能完成決策休吠。所以扳埂,接下來我們先從語言開始,一步一步來分析下在你的項(xiàng)目中選擇Go是否合適瘤礁。
Go在高并發(fā)編程方面無疑是出眾的阳懂,通過goroutine從語言層面支持了協(xié)程,這是Java等語言所無法比擬的柜思,這也是大多數(shù)人在面對高并發(fā)場景選擇Go的重要原因之一岩调。雖然Java有Kilim之類的框架,但沒有語言層的支持始終稍遜一籌赡盘。
除此之外号枕,Go的其他語法也很有趣,比如多返回值陨享,在一定程度上為開發(fā)者帶來了一定的便利性葱淳。試想钝腺,為了返回兩到三個值,不得不封裝一個對象赞厕,或者抹去業(yè)務(wù)名稱使用Map艳狐、List等集合類,高級一點(diǎn)用Apache的Pair和Triple皿桑,雖然可行毫目,但始終不如Go的實(shí)現(xiàn)來得優(yōu)雅。在此之上诲侮,Go也統(tǒng)一了異常的返回方式镀虐,不用再去糾結(jié)是通過拋異常還是錯誤碼來判斷是否成功,多返回值的最后一個是Error就行了沟绪。
Go在語言的原生類型中支持了常用的一些結(jié)構(gòu)粉私,比如map和slice,而其他語言中它們更多是存在于庫中近零,這也體現(xiàn)了這門語言是從實(shí)踐角度出發(fā)的特點(diǎn)诺核,既然人人都需要,為什么不在語言層面支持它呢久信。函數(shù)作為一等公民出現(xiàn)在了Go語言里窖杀,不過Java在最近的Java 8中也有了Lambda表達(dá)式,也算是有進(jìn)步了裙士。
其他的一些特性入客,則屬于錦上添花型的,比如不定參數(shù)腿椎,早在2004年的Java 1.5中就對varargs有支持了桌硫;多重賦值在Ruby中也有出現(xiàn),但除了多返回值賦值啃炸,以及讓你在變量交換值時少寫一個中間變量铆隘,讓代碼更美觀一些之外,其他的作用著實(shí)不是怎么明顯南用。
說了這么多Go的優(yōu)點(diǎn)膀钠,當(dāng)然它也有一些問題,比如GC裹虫,說到它肿嘲,Java不得不露出潔白的牙齒,雖然在大堆GC上G1還有些不盡如人意筑公,但Java的GC已經(jīng)發(fā)展了很多年雳窟,各種策略也比較成熟,CMS或G1足以應(yīng)付大多數(shù)場景匣屡,實(shí)在有要求還能用Azul Zing JVM封救。不過從最新的Go 1.5的消息來看际长,Go的GC實(shí)現(xiàn)有了很大地提升,順便一提的是GOMAXPROCS默認(rèn)也從1變成了CPU核數(shù)兴泥,看來官方對Go在多核的利用方面更有信心了。
許世偉在《Go 語言編程》的前言中預(yù)言未來10年虾宇,Go會取代Java搓彻,位居編程榜之首,當(dāng)時是2012年嘱朽,為了看看2009年TIOBE年度編程語言如今的排名旭贬,筆者在撰寫本文時特意去TIOBE看了下,最近的2015年8月排行榜搪泳,Java以19.274%位居榜首稀轨,Go已經(jīng)跌出了前50,這不禁讓人有些意外岸军。
但總體上來說奋刽,筆者認(rèn)為Go在語言層面的表現(xiàn)還是相當(dāng)出色的,解決了一些編程中的痛點(diǎn)艰赞,學(xué)習(xí)曲線也能夠接受佣谐,特別是對于那些有C/C++背景的人,會感覺十分親切方妖。
工程問題
一個人寫代碼時可以很隨性狭魂,想怎么寫就怎么寫,但當(dāng)一個人變成一個團(tuán)隊(duì)后党觅,這種隨性或者說隨便就會帶來很多問題雌澄,于是就誕生了編碼規(guī)范這玩意兒,大廠基本都有自己的編碼規(guī)范杯瞻,比如Google就有針對不下十種編程語言的規(guī)范镐牺。團(tuán)隊(duì)內(nèi)約定一套編碼規(guī)范能夠很大程度上地確保代碼的風(fēng)格,降低閱讀溝通的成本魁莉。Go內(nèi)置了一套編碼規(guī)范任柜,違反了該規(guī)范代碼就無法編譯通過,可以說只要你是寫Go的沛厨,那你的代碼就不會太難看宙地,當(dāng)然Go也沒有把所有東西就強(qiáng)制死,還有一些推薦的規(guī)范可以通過gofmt進(jìn)行格式化逆皮,但這步不是必須的宅粥。
雖然Go自己解決了這個問題,但并不能說Java在這方面是空白电谣,Java發(fā)展至今周邊工具無數(shù)秽梅,并不缺成熟的代碼靜態(tài)分析工具抹蚀,比如CheckStyle、PMD和FindBugs企垦,它們不僅能掃描編碼規(guī)范的問題环壤,甚至還能掃描代碼中潛在的問題并給出解決方案,并且使用方便钞诡,在Java開發(fā)者社區(qū)中有很高地接受度郑现,應(yīng)該說大多數(shù)靠譜地開發(fā)者都會使用這些工具。除此之外荧降,一些大廠也有自己的強(qiáng)制手段接箫,比如百度內(nèi)部也有很多語言的編碼規(guī)范,而且大部分情況下如果沒有通過編碼規(guī)范的掃描朵诫,你是無法提交代碼的辛友;還有一些公司會在持續(xù)集成過程中加入代碼掃描,有FindBugs高優(yōu)先級的問題時必須修復(fù)才能進(jìn)入下一個階段剪返。所以說Go在這個問題上的優(yōu)勢并不明顯废累,或者說在一個成熟的環(huán)境下,這只是合格而已脱盲。
這里需要強(qiáng)調(diào)筆者的一個觀點(diǎn):
Go在語言本身和發(fā)行包中融入了很多最佳實(shí)踐九默,正是這些前人的經(jīng)驗(yàn)才讓它看起來如此優(yōu)秀。拿這么個海陸空混編特種部隊(duì)去和Java宾毒、C驼修、Ruby這些語言本身做對比,顯得不太公平诈铛,所以本文在考慮問題時都會結(jié)合語言及其生態(tài)圈中的成員乙各,畢竟這才更接近真實(shí)的情況。
Go本身對項(xiàng)目結(jié)構(gòu)有一套約定幢竹,代碼放哪里耳峦,測試文件如何命名,編譯打包后的結(jié)果輸出到哪個目錄焕毫,甚至還有g(shù)o cover這種統(tǒng)計測試覆蓋率的命令行蹲坷,開發(fā)者不用在這些問題上太過糾結(jié),再一次體現(xiàn)了Go注重工程實(shí)踐的特點(diǎn)邑飒⊙回過頭來,Java方面疙咸,Maven县匠、Gradle都是注重于工程生命周期管理的工具,而且Maven更是歷史悠久,被廣泛用于各種項(xiàng)目之中乞旦。以Maven為例贼穆,不僅能夠?qū)崿F(xiàn)上述所有功能,還有很強(qiáng)的插件擴(kuò)展能力兰粉,這里需要的只是一次性維護(hù)好pom.xml文件就行了故痊,由于Maven的使用群很大,網(wǎng)上有大量的范例玖姑,甚至還有很多生成工程的工具和模板愕秫,所以使用成本并不高。
這里還要衍生出一個話題客峭,就是依賴管理,在開發(fā)代碼時抡柿,勢必需要依賴很多外部的東西舔琅,Go可以直接import遠(yuǎn)程的內(nèi)容,這個特性很有創(chuàng)意洲劣,但并不能很好地解決版本的問題备蚓,在Maven或Gradle里,我們可以直接指定各個依賴項(xiàng)甚至是插件的版本囱稽,工具會自動從倉庫中下載它們郊尝。如果需要同時在同一個系統(tǒng)的不同模塊里依賴同一個庫的不同版本,我們還能夠通過OSGi這種略顯復(fù)雜的手段來實(shí)現(xiàn)战惊,在模塊化方面流昏,Jagsaw雖然被一延再延,但估計有望納入Java 9吞获,這個特性也會解決不少問題况凉。而根據(jù)Golang實(shí)踐群中大家的討論,似乎godep各拷、gb和gvt都不盡如人意刁绒,在這點(diǎn)上看來Go還有一段路要走。
綜上所述烤黍,Go在工程方面的確有不少亮點(diǎn)知市,吸納了很多最佳實(shí)踐,甚至可以說用Go之后更容易寫出規(guī)范的代碼速蕊,有好的項(xiàng)目結(jié)構(gòu)嫂丙,但與生態(tài)圈完備的Java相比,Go并不占優(yōu)勢规哲,因?yàn)樽罱K代碼的質(zhì)量還是由人決定的奢入,雙方都不缺好的工具,所以這方面的特點(diǎn)并不能影響技術(shù)選型的決策。
開發(fā)實(shí)踐
Talk is cheap. Show me the code.
下面進(jìn)入編碼環(huán)節(jié)腥光,先從Go引以為傲的并發(fā)開始关顷,《Go語言編程》的前言中有這樣一段代碼:
func run(arg string){// ...}funcmain(){go run("test")...}
書中與之對比的Java代碼有12行,而且還是線程武福,不是協(xié)程议双,對比很明顯,但那是在2012年的時候捉片,時至今日平痰,Java已經(jīng)發(fā)展到了Java 8,3年了伍纫,看看如今的Java代碼會是什么樣的:
public class ThreadDemo{publicstaticvoidmain
(String[]args){Stringstr="test";
// 為了和原先的Java版本對照宗雇,說明能傳參進(jìn)入線程內(nèi),
在外聲明了一個字符串莹规,其實(shí)可以直接寫在Lambda里
new Thread(()->
{/* do sth. with str */}).start();}}
不是協(xié)程仍是硬傷赔蒲,但有了Lambda表達(dá)式,代碼短了不少良漱。不過話又說回來舞虱,這樣的比較并沒有太多意義,所以各位Go粉也不用站出來說Go也支持閉包母市,Go的版本也能精簡矾兜。我們比的不是誰寫的短,在Java實(shí)踐中患久,大多數(shù)時候大家會選擇線程池椅寺,而不是自己new一個Thread對象,Doug Lea大神的Java并發(fā)包非常的好用蒋失,而且很靠譜配并。另外,并發(fā)中處理的內(nèi)容才是關(guān)鍵高镐,新啟一個線程或者協(xié)程才是萬里長城的第一步溉旋,如果其中的業(yè)務(wù)邏輯有10個分支,還要多次訪問數(shù)據(jù)庫并調(diào)用遠(yuǎn)程服務(wù)嫉髓,那無論用什么語言都白搭观腊。所以在業(yè)務(wù)邏輯復(fù)雜的情況下,語言的差異并不會太明顯算行,至少在Java和Go的對比下不明顯梧油,至于其他更高階、表達(dá)力更強(qiáng)的語言(比如Common Lisp)州邢,大家就要拼智商了儡陨。
還有一些情況中,由于客觀因素制約,完全就無法使用Go骗村,比如現(xiàn)在如火如荼的互聯(lián)網(wǎng)金融系統(tǒng)里嫌褪,與銀行對接的系統(tǒng)幾乎沒有選擇,都是Java實(shí)現(xiàn)的胚股,因?yàn)橛械你y行只會給Jar包啊……給Jar包啊……Jar包啊……如果是個so文件笼痛,也許還能用cgo應(yīng)付一下,面對一個Jar你讓Go該何去何從琅拌?
拋開這些讓人心煩的問題缨伊,讓我們再來看看現(xiàn)在比較常見的如何實(shí)現(xiàn)REST服務(wù)。說到這里进宝,就一定要祭出國人出品的Beego框架刻坊。一個最簡單的REST服務(wù)可以是這樣的:
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方面基本可以算是事實(shí)標(biāo)準(zhǔn)隶校,而Spring Boot更是大大提升了Spring項(xiàng)目的開發(fā)效率漏益,看看同樣實(shí)現(xiàn)一個REST服務(wù)蛹锰,在SpringBoot里是怎么做的深胳。
首先,到start.spring.io根據(jù)需要生成項(xiàng)目骨架(其實(shí)完全可以方便地自己通過Maven手工配置依賴或者是用CLI工具來創(chuàng)建)铜犬,為了后續(xù)的演示舞终,這里我會選上“Web”、“Actuator”和“Remote Shell”癣猾,其實(shí)就是多了兩個Maven的依賴敛劝,下文運(yùn)維部分會提到,然后隨便找個順手的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);? ? }
}
運(yùn)行這段代碼會自動啟動內(nèi)置Tomcat容器像捶,訪問http://localhost:8080/就能看到輸出了上陕。因?yàn)槠鋵?shí)就是Spring,所以可以毫無壓力地與其他各種框架設(shè)施組合拓春,也沒有太多學(xué)習(xí)成本释簿。
可見兩者在實(shí)現(xiàn)REST服務(wù)方面,并沒有太大的差別硼莽,加之上文提到的業(yè)務(wù)邏輯問題庶溶,只要運(yùn)用恰當(dāng)?shù)墓ぞ撸瑑煞N語言之間并不會產(chǎn)生質(zhì)的差異。
Beego中的ORM支持MySQL偏螺、PostgreSQL和Sqlite3行疏,而在Java里Hibernate和myBatis這樣的ORM工具幾乎能通吃大多數(shù)常見的關(guān)系型數(shù)據(jù)庫,且相當(dāng)成熟砖茸,社區(qū)配備了各種自動生成工具來簡化使用隘擎,行業(yè)里還有JPA這樣的公認(rèn)標(biāo)準(zhǔn)×购唬縱觀Go的ORM工具货葬,大家還是在探討,究竟哪個才好用呢劲够?切到NoSQL方面震桶,雙方都有大量的驅(qū)動可以使用,比如MongoDB和Redis都有詳盡的驅(qū)動列表征绎,MongoDB還沒有官方驅(qū)動蹲姐,但有社區(qū)維護(hù)的mgo,算是打成平手吧人柿。再大一點(diǎn)柴墩,像用到Hadoop、Spark和Storm的場景下凫岖,似乎Java的出鏡率更高江咳,或者是直接通過Streaming方式就解決了,此處也就不再展開了哥放。
雖然說了這么多問題歼指,但如果真的遇到了大流量、高并發(fā)的場景甥雕,需要從頭開始開發(fā)用來處理這些問題的基礎(chǔ)設(shè)施時踩身,Go還是不錯的選擇。比如社露,七牛這樣的云服務(wù)提供商挟阻,又或者是BFE(Baidu Front End,號稱可能是全世界流量最大的Go語言集群 峭弟,在2015年的Velocity大會上留下了它的身影——圖1和圖2)這樣的硬貨附鸽,請不要糾結(jié)。
運(yùn)維
寫完代碼只是萬里長征的一小步孟害,后面還有一大堆的事情等著你去解決拒炎,比如怎么把寫完的代碼編譯邑狸、打包梨与、發(fā)布上線棋恼。編譯打包就不說了,Go的命令行工具go build就能直接把你的代碼連同它的所有依賴一起打成一個可執(zhí)行文件更扁。至于部署膏斤,大家都稱贊Go的部署沒有依賴(除了對glibc的版本有要求俊扭,不考慮需要cgo的情況)已添,直接把可執(zhí)行文件往那里一扔就好了,非常方便鸿摇。Go內(nèi)置了強(qiáng)大的HTTP支持石景,不需要其他Web服務(wù)器來做支撐就能獲得不錯的性能。
再來看看Java拙吉,按照常理潮孽,一般都會使用Maven或者Gradle來處理編譯、打包筷黔,甚至是發(fā)布往史,仍舊以Maven為例,mvn package就能完成編譯和打包佛舱∽道可以選擇Jar包,如果是Web項(xiàng)目部署到容器里的話可以是War包请祖,也可以將各種資源打包到一起放到壓縮包(zip订歪、tar等等)里,這個步驟并不復(fù)雜肆捕。
接下來的部署環(huán)節(jié)刷晋,大家就有話要說了,“Write Once, Run Anywhere”這曾是Java的宣傳語福压,但正是這句話一直被大家詬病掏秩,其實(shí)如果代碼中不使用平臺特定的內(nèi)容(比如避免綁定在WebLogic上)或舞,不使用某個特定版本JDK的內(nèi)部類(比如com.sun里的東西荆姆,這種做法本來就不推薦),Java的代碼還是能夠做到編譯后在任何地方都能運(yùn)行的映凳,事實(shí)上現(xiàn)在絕大部分情況下胆筒,大家也都是這么做的,看看廣大的Java庫都是發(fā)布Jar到Maven倉庫的诈豌,也沒誰讓你直接拉源碼來編譯仆救。在不同的環(huán)境下,只需要部署了對應(yīng)的JDK就好了(一般放到裝機(jī)模板里矫渔,或者直接拿安裝包部署一下就好了)彤蔽,至于是什么操作系統(tǒng)其實(shí)并不重要。
延續(xù)上文REST服務(wù)的例子庙洼,Java的Web項(xiàng)目一般都會部署到容器里顿痪,比如Tomcat或者Jetty镊辕,當(dāng)然也有用商業(yè)容器的(很多銀行就是用的WebLogic),所以大家就都認(rèn)為部署Java程序需要先有容器蚁袭,這其實(shí)是幾年前的事情了征懈,后來刮起了一股內(nèi)嵌容器的風(fēng)潮,Tomcat和Jetty都可以嵌入到你的程序里揩悄,再也不用為有沒有容器而煩惱了卖哎。Spring Boot索性把這件事變得更簡單了,mvn package后删性,一句話就能搞定內(nèi)置Tomcat的啟動亏娜、完成各種部署,然后一切就變成下面這樣(假設(shè)最后生成的Jar包名為demo.jar):
java-jar demo.jar
在Spring Boot 1.3里蹬挺,還能通過調(diào)整Maven Plugin的配置照藻,讓Jar可以直接執(zhí)行(不要小看這么一個變化,它可以大大提升可運(yùn)維性):
./demo.jar
所以說Java程序難部署其實(shí)也是歷史汗侵,現(xiàn)在的Java程序部署早已是另一番光景幸缕。兩者的編譯、打包晰韵、部署環(huán)節(jié)完全可以打成平手发乔。筆者認(rèn)為有些方面Java反而更勝一籌,比如Java基本就不用操心交叉編譯的問題雪猪;Go的庫在發(fā)布時推薦直接發(fā)布源碼而非二進(jìn)制包栏尚,遇到天朝特有的網(wǎng)絡(luò)無法訪問的情況,編譯個東西還要自備梯子……至于和Nginx等等的配合只恨,更是大家都很方便译仗,就不再贅述了。
完成了部署官觅,接下來的日志和監(jiān)控纵菌,都是很常規(guī)的問題,日志各自有對應(yīng)的庫休涤,而監(jiān)控都是依賴專業(yè)的監(jiān)控平臺咱圆,自己做好信息輸出就好了,請容我再秀一下Spring Boot的RemoteShell終端監(jiān)控功氨,除了常規(guī)的HTTP方式輸出JSON信息(自帶了健康檢查序苏、儀表數(shù)據(jù)、Dump捷凄、請求跟蹤等一系列REST輸出)忱详,還自帶了這么個類似top的高大上的玩意兒,ssh -p 2000 user@localhost后執(zhí)行dashboard可以看到這個實(shí)時更新的界面跺涤。
總結(jié)
說了這么多匈睁,來總結(jié)下全文的觀點(diǎn)——雖然Go在語言上表現(xiàn)的很出色管钳,也融入了很多最佳實(shí)踐,但是結(jié)合多方考慮软舌,在很多情況下它并不會比Java帶來更多價值才漆,甚至還不一定能做的比Java好,因此作為一個Java程序員佛点,我不會在自己的生產(chǎn)項(xiàng)目中轉(zhuǎn)向Go醇滥。
此外,除了本文重點(diǎn)討論的那些問題超营,還有更現(xiàn)實(shí)的問題擺在那里鸳玩,比如團(tuán)隊(duì)轉(zhuǎn)型成本和招聘的成本,千萬不要小看招聘演闭,對于管理者而言不跟,招聘也是工作中的重要內(nèi)容,試想一下米碰,是招個有經(jīng)驗(yàn)的Go程序員容易窝革,還是招一個有經(jīng)驗(yàn)的Java程序員容易,就算能招到一個會Go的正式員工吕座,你能招到一個會Go的外包么虐译,特別是在團(tuán)隊(duì)急需補(bǔ)充新鮮血液時,結(jié)果是顯而易見的吴趴。
但這一切都不妨礙大家來學(xué)習(xí)Go漆诽,本文開頭就已經(jīng)表達(dá)過這一觀點(diǎn),業(yè)余時間學(xué)習(xí)Go和在生產(chǎn)項(xiàng)目中不用Go并不沖突锣枝,Go還是有很多值得學(xué)習(xí)和借鑒的地方厢拭,而且誰也說不準(zhǔn)哪天你就真遇上了適合用Go的項(xiàng)目呢。