一名Java開發的Go語言學習筆記(一)

泊浮目發表於2023-05-15
版本日期備註
1.02023.5.15文章首發

本文首發於泊浮目的掘金:https://juejin.cn/user/1468603264665335

0. 概要

最近因為業務需要在學Go語言,雖然之前也用過別的語言,但主力語言一直是Java。在這裡也想主要想用Java做對比,寫幾篇筆記。

這篇主要是講語言本身及較為表面的一些對比。

這裡的對比用的是Java8,Go的版本是1.20.2。

1. Compile與Runtime

  • 在靜態、動態連結支援方面,兩者相同。
  • Go在Runtime時,程式結構是封閉的。但Java並不是,基於Classloader的動態載入,實現許多靈活的特性,比如Spring,FlinkSQL。但這樣做會讓Java應用的Startup時間更長。
  • Java的Runtime久經打磨,也是面向長時間應用設計。
  • Go直接編譯成可執行檔案,而Java是先編譯成Class檔案,然後JVM去解釋執行。

有興趣的同學可以看我之前的的一篇筆記:《筆記:追隨雲原生的Java》

2. 命名規範

  • Go語言在變數命名規範遵循的是C語言風格,越簡單越好。
  • Java建議遵循見名知意。
  • 比如:

    • 下標:Java建議index,Go建議i
    • 值:Java建議value,Go建議v
  • 我認為語言上偏簡單的設計,則對工程師的能力要求更強。

3. 標準庫對於工程能力的支援

  • 無論是Format還是Test以及模組管理,Go都是開箱即用的,比較舒服。如果標準庫的確很好用、社群的迭代能力強,那這是個好事,現在看來就是。
  • Java對於這塊都是經過了長期的發展,相關的工具鏈比較成熟,相當於是物競天擇留下來的。

4. Composite litera(複合字面值)

可能沒人聽過這個說法,舉幾個例子:

m := map[int]string {1:"hello", 2:"gopher", 3:"!"}

複合字面值由兩部分組成:一部分是型別,比如上述示例程式碼中賦值運運算元右側的map[int]string;另一部分是由大括號{}包裹的字面值。

在宣告物件時,也有類似的用法:

// $GOROOT/src/net/http/transport.go
var DefaultTransport RoundTripper = &Transport{
    Proxy: ProxyFromEnvironment,
    DialContext: (&net.Dialer{
        Timeout:   30 * time.Second,
        KeepAlive: 30 * time.Second,
        DualStack: true,
    }).DialContext,
    MaxIdleConns:          100,
    IdleConnTimeout:       90 * time.Second,
    TLSHandshakeTimeout:   10 * time.Second,
    ExpectContinueTimeout: 1 * time.Second,
}
 

Go推薦使用field:value的複合字面值形式對struct型別變數進行值構造,這種值構造方式可以降低結構體型別使用者與結構體型別設計者之間的耦合,這也是Go語言的慣用法。

這個是真的很香,Groovy和Kotlin也有類似的支援,很方便使用。尤其是建構函式特別長的時候,你可能說builder也可以做到,但誰不想少寫幾行程式碼呢。

5. 對於程式設計正規化的支援

  • 在Java中,類是一等公民。支援繼承、組合,對函式語言程式設計有一定的支援。
  • 在Go中,函式是一等公民(多返回值),對於函式語言程式設計支援較為完備。對物件導向完全透過組合來實現。
  • 從傳參是否可以傳函式上,我們就可以看出Go的支援比Java好。Java傳參中,傳一個函式,其實是透過一個匿名物件來傳,而Go是真正的一個函式。Kotlin在這塊相對Java好了點,至少在編寫時。
  • 在Java中,你想寫個工具函式,也要先宣告一個類再寫進去。略Verbose,其實這個類我們不會把它new出來,只是為了放個函式,所以我們寫了個類。但實際用的時候,XxxUtils.method,前面的Xxx其實有一定的提醒作用,可以作為一個上下文來猜測裡面的邏輯。但是如果我們在method裡寫清楚,當然也可以做到同樣的功效,所以這點來說Go是比較舒服的。
  • Go的物件方法宣告方式比較特殊:
//宣告一個型別
type MyInt int
//繫結一個方法
//func後面的()裡,相當於宣告瞭這個方法繫結的型別。在Go語言裡叫做recevier,一個函式只能有一個recevier,且不能是指標、介面型別。
//不能橫跨Go包為其他包內的自定義型別定義方法。
func (i MyInt) String() string {
    return fmt.Sprintf("%d", int(i))
}
//在編譯期,會把方法的第一個引數變成recevier。很簡單的實現。有點像Koltin中的Extension Properties。
  • Go的Interface是隱式的,只要你實現了對應的方法,就是這個Interface的實現。這個在一開始使用的時候會很不適應,但這個松耦的一種體現——隱式的interface實現會不經意間滿足依賴抽象、里氏替換、介面隔離等設計原則。
  • Go並沒有繼承。類似的做法叫做型別嵌入(type embedding)的方式。簡單來說就是你有一個T1,T2型別,他們有各自的方法,當你宣告一個T型別時幷包含了T1,T2型別的field,那麼T就擁有了T1,T2的方法。這個實現的確比繼承舒服多了,繼承很容易寫出一些壞味道的程式碼。這是一種委派思想的實現(delegate)。JS中原型鏈從外表看起來也有點像這種。
  • Go的方式支援多返回值。這點是比較舒服的,如果在Java中要返回多個值,就要考慮封裝成物件了,比如Tuple2,Tuple3...之類的,這在其他的一些JVM語言中隨處可見。

6. 異常流:Error與Exception

  • Go裡面的error相當於Java的可檢異常,Panic相當於Java的RuntimeException和Error。
  • 如果你覺得Go裡面大量的if err != nil讓程式碼巢狀時,可以看看一些最佳化if else的技巧,比如我部落格裡有。
  • 總的來說,像是在不同的實現做同一件事。也是個仁者見仁智者見智的事。

7. 併發

  • Java用的POSIX原語義的執行緒。而Go是自己實現了一套使用者態的執行緒,或者說叫協程。
  • POSIX原語義的執行緒總體來說易用性沒這麼好,需要牢記一些知識點才可以避免踩坑。Go在這點上比較友好。
  • 效能上,由於實踐時一般Java會用執行緒池,所以建立、銷燬的代價還好。其實Go也有自己的執行緒池,用執行緒去綁多個協程。但在上下文切換上,的確是POSIX原語義的執行緒代價會大點。
  • 為了避免一個協程把執行緒獨佔住,在編譯期、以及一些標準庫API上都要做縝密的設計。

相關文章