Go:語法糖的代價

在 Go 語言中,你可以用少量的代碼表達很多東西。您通常可以查看一小段代碼並清楚地瞭解此程序的功能。這在 Go 社區中被稱爲地道的 Go 代碼。保持跨項目代碼的一致性需要持續不斷地努力。

當我遇到 Go 的部分看起來不像地道 Go 代碼時,這通常是有原因的。最近,我注意到 Go 中的接口切片 (或抽象數組) 工作方式有點怪異。這種怪異有助於理解在 Go 中使用複雜類型會帶來一些成本,而且這些語法糖 [1] 並不總是沒有代價的。爲深入瞭解問題出現的原因,我對遇到的行爲進行拆分,這助於闡明 Go 的一些設計原則。

舉例說明

我們將編寫一個小型程序,它定義一個動物列表 (例如,dogs),並調用一個函數,將每個動物的噪聲輸出到控制檯

animals := []Animal{Dog{}}
PrintNoises(animals)

程序成功通過編譯,並在控制檯打印出了 “Woof!”。下面就是這個程序的類似的版本:

dogs := []Dog{Dog{}}
PrintNoises(dogs)

程序無法編譯,並將以下錯誤打印到控制檯,而不是輸出 "Woof!"

cannot use dogs (type []Dog) as type []Animal in argument to PrintNoises

如果你熟悉 Go,你可能會認爲應該檢查一下 Dog 實現了 Animal,對吧? 如果是實現錯誤,它的輸出應該類似於

cannot use dogs (type []Dog) as type []Animal in argument to PrintNoises: []Dog does not implement []Animal (missing Noise method)

爲什麼第一個程序可以用 Dog 作爲 Animal 來編譯和運行,而第二個程序卻不能,即使它們看起來都是地道的和正確的

下面是本例中用作參考的其餘代碼。你可以通過編譯它,來了解上述用法的內部原理

type Animal interface {
  Noise() string
}

type Dog struct{}

func (Dog) Noise() string {
  return "Woof!"
}

func PrintNoises(as []Animal) {
  for _, a := range as {
    fmt.Println(a.Noise())
  }
}

進一步簡化問題

讓我們試着用一種更簡單的方法來複現這個問題,以便更好地理解它。靜態類型檢查是一種有用的 Go pattern,用於斷言類型是否實現了接口。讓我們先檢查一下 Dog 是否實現了 Animal

var _ Animal = Dog{}

上面代碼編譯成功。那我們接下來就檢查程序中用到的 slices

var _ []Animal = []Dog{}

上面代碼沒有編譯成功,編譯器報錯:

cannot use []Dog literal (type []Dog) as type []Animal in assignment

現在,我們已經復現了一個與例子類似 (但不是完全相同) 的錯誤。利用這些不同的線索,我做了一些研究來找出如何解決這個問題,以及爲什麼會發生這樣的事情

尋找解決方案

在做了一些研究之後,我發現了兩件事: 一個是解決方案,另一個是背後的原理。我們從修正程序開始,因爲它有助於說明基本原理

下面是最初未能編譯的代碼的一個修復:

dogs := []Dog{Dog{}}
// 新邏輯: 把 dogs 的切片轉換成 animals 的切片
animals := []Animal{}
for _, d := range dogs {
  animals = append(animals, Animal(d))
}
PrintNoises(animals)

通過將 Dog 的切片轉換爲 Animal 的切片,現在可以將其傳入 Printnoise 函數併成功運行。當然,這看起來有點傻,因爲它基本上是已經運行的第一個程序的冗長版本。然而,在一個更大的項目中,這一點可能並那麼明顯。修復的代價是多了四行代碼。這四行代碼似乎是多餘,直到你開始考慮作爲開發人員必須修復它的原因

尋找原理

現在你知道如何修復它,我們來探究它背後的原理。我找到了不錯的解析:go 不支持切片中協變 [2](譯者注: 協變 [3]: 原文單詞爲 covariance, 是指在計算機科學中,描述具有父 / 子型別關係的多個型別通過型別構造器、構造出的多個複雜型別之間是否有父 / 子型別關係的用語)

換句話說,Go 不會執行導致 O(N) 線性操作的類型轉換 (如切片的情況),而是將責任委託給開發人員。也就是說,執行這種類型的轉換是有成本的。不過,Go 並不是每次都這樣做。例如,當將字符串轉換爲 []byte 節時,Go 將免費爲您執行這種線性轉換,這可能是因爲這種轉換通常很方便。這只是語言中語法糖的衆多例子之一。對於切片 (和其他非基本類型),Go 選擇不爲您承擔執行此操作的額外成本

這是有道理的——在我使用 Go 的 3 年裏,這是我第一次遇到這種情況。這可能是因爲 Go 在語法中灌輸了 “simpler is better” 的思想

結論

一門語言的作者通常會在語法糖方面做出權衡,有時他們會添加功能,即便這會使語言變得更加臃腫,有時他們會將成本轉嫁給開發人員。我認爲,不隱式地執行高開銷的操作的決定在保持 Go 語言地道、整潔、可控上有積極的影響。

上面的例子只是這個道理的一個應用。這個例子表明,熟悉一種語言的習慣用法有副作用。對設計決策保持深思熟慮總是一個好主意,而不是期望語言或編譯器能幫到你。

我鼓勵您在 Go 中尋找更多存在權衡語法的地方。它能幫助你更好地理解這門語言。我亦如此。

引用

以下是本文的引用:

via: https://medium.com/@asilvr/the-cost-of-syntactic-sugar-in-go-5aa9dc307fe0

作者:Katy Slemon[9] 譯者:Alex1996a[10] 校對:lxbwolf[11]

本文由 GCTT[12] 原創編譯,Go 中文網 [13] 榮譽推出,發佈在 Go 語言中文網公衆號,轉載請聯繫我們授權。

參考資料

[1]

語法糖: https://en.wikipedia.org/wiki/Syntactic_sugar

[2]

go 不支持切片中協變: https://www.reddit.com/r/golang/comments/3gtg3i/passing_slice_of_values_as_slice_of_interfaces/

[3]

協變: https://zh.wikipedia.org/wiki/%E5%8D%8F%E5%8F%98%E4%B8%8E%E9%80%86%E5%8F%98

[4]

GitHub Gist of the above example: https://gist.github.com/asilvr/4d4da3cdc8180c5a9740d2890d833923

[5]

Go 語言官網: https://golang.org

[6]

Thread on covariance in Go: https://www.reddit.com/r/golang/comments/3gtg3i/passing_slice_of_values_as_slice_of_interfaces/

[7]

Big-O notation: https://en.wikipedia.org/wiki/Big_O_notation

[8]

Syntactic sugar: https://en.wikipedia.org/wiki/Syntactic_sugar

[9]

Katy Slemon: https://medium.com/@katyslemon

[10]

Alex1996a: https://github.com/Alex1996a

[11]

lxbwolf: https://github.com/lxbwolf

[12]

GCTT: https://github.com/studygolang/GCTT

[13]

Go 中文網: https://studygolang.com/

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/-lps3sl-riDOLxUIUyIrWA