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 中尋找更多存在權衡語法的地方。它能幫助你更好地理解這門語言。我亦如此。
引用
以下是本文的引用:
-
GitHub Gist of the above example[4]
-
Go 語言官網 [5]
-
Thread on covariance in Go[6]
-
Big-O notation[7]
-
Syntactic sugar[8]
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