Go 1.20新變化!第一部分:語言特性
又到了 Go 發(fā)布新版本的時(shí)刻了!
2022 年第一季度的 Go 1.18 是一個(gè)主版本,它在語言中增加了期待已久的泛型,同時(shí)還有許多微小功能更新[1]與優(yōu)化[2]。2022 年第三季度的 Go 1.19 是一個(gè)比較低調(diào)[3]的版本。
現(xiàn)在是 2023 年,Go 1.20 RC 版本[4]已經(jīng)發(fā)布,而正式版本也即將到來,Go 團(tuán)隊(duì)已經(jīng)發(fā)布了版本說明草案[5]。
在我看來,Go 1.20 的影響介于 1.18 和 1.19 之間,比 1.19 有更多的功能更新并解決了一些長(zhǎng)期存在的問題,但沒有達(dá)到 1.18 中為語言增加泛型這樣的重磅規(guī)模。
盡管如此,我還是要把我對(duì)“Go 1.20 的新變化”的看法分成系列三篇博文。
首先,我寫了 Go 1.20 中的語言變化(如下),在下一篇文章中,我將寫標(biāo)準(zhǔn)庫(kù)的重要變化,最后一篇將講解 Go 1.20 中我最喜歡的對(duì)標(biāo)準(zhǔn)庫(kù)的小改動(dòng)。那么,讓我們來看看語言方面的變化。首先,對(duì)泛型的規(guī)則做了一個(gè)小小的修改。有了 Go 泛型,你可以通過一個(gè)函數(shù)獲取任何map的鍵:
func keys[K comparable, V any](m map[K]V) []K {
var keys []K
for k := range m {
keys = append(keys, k)
}
return keys
}
···
在這段代碼中,K comparable, V any 為“類型約束”。這意味著 K 可以是任何 comparable 的類型,而 V 則沒有類型限制。comparable 類型為數(shù)字、布爾、字符串和由 comparable 元素組成的固定大小的復(fù)合類型等。因此,K 為 int,V 為一個(gè) bytes 切片是合法的,但 K 是一個(gè) bytes 切片是非法的。
我說過上面的代碼會(huì)給你任何 map 的鍵,但在 Go 1.18 和 1.19 中,這并不是完全正確的。如果你試圖把它用在一個(gè)鍵值為接口類型的 map 上,它將不會(huì)被編譯。
m := make(map[any]any) // ok
keys(m)
// 編譯器錯(cuò)誤(Go 1.19):any 沒有實(shí)現(xiàn) comparable
···
這個(gè)問題歸結(jié)為圍繞 K comparable 含義的解讀。要作為 map 鍵使用,類型必須被 Go 編譯器認(rèn)為是 comparable 的。例如,這是無效的:
m := make(map[func()]any)
// 編譯器錯(cuò)誤:無效的 map 鍵類型 func()
···
然而,你可以通過使用接口來得到一個(gè)運(yùn)行時(shí)錯(cuò)誤而不是編譯器錯(cuò)誤:
m := make(map[any]any) // 正確
k := func() {}
m[k] = 1 // panic:運(yùn)行時(shí)錯(cuò)誤:哈希值為不可哈希的類型 func()
所以,像 any 這樣的接口類型是 map 的有效鍵類型,但如果你試圖把一個(gè)缺少有效類型定義的鍵放到 map 中,就會(huì)在運(yùn)行時(shí)出現(xiàn) panic 錯(cuò)誤。顯然,沒有人希望他們的代碼在運(yùn)行時(shí)出現(xiàn) panic 錯(cuò)誤,但這是在 map 中允許動(dòng)態(tài)類型鍵的唯一方法。
下面是一個(gè)從不同角度看同一問題的例子。假設(shè)我有一個(gè)這樣的錯(cuò)誤類型:
type myerr func() string
func (m myerr) Error() string {
return m()
}
而現(xiàn)在我想使用自定義的錯(cuò)誤類型進(jìn)行比較:
var err1 error = myerr(func() string { return "err1" })
var err2 error = myerr(func() string { return "err2" })
fmt.Println(err1 != nil, err2 != nil) // true true
fmt.Println(err1 == err2)
// panic:運(yùn)行時(shí)錯(cuò)誤:對(duì) main.myerr 不可比類型進(jìn)行比較
正如你所看到的,一個(gè)接口值在編譯時(shí)被認(rèn)為是 comparable 類型,但是如果它被賦的值是一個(gè)“不可比類型”,則在運(yùn)行時(shí)就會(huì)出現(xiàn) panic。如果你試圖比較兩個(gè) http.Handler,而它們恰好都是 http.HandlerFuncs,你同樣可以看到這個(gè)問題。
當(dāng) Go 1.18 支持了泛型后,大家發(fā)現(xiàn)[6],由于接口在編譯時(shí)被認(rèn)為是 ,但可能會(huì)包含不可比較的具體類型。如果你寫的泛型代碼的類型約束是 comparable,但錯(cuò)誤的值被存儲(chǔ)在一個(gè)接口中,就有可能出現(xiàn)運(yùn)行時(shí) panic。保守起見,Go 團(tuán)隊(duì)決定[7]在評(píng)估(此特性)的全部影響階段,Go 1.18 限制使用接口作為 comparable 類型。
現(xiàn)在已經(jīng)過了一年了,也發(fā)布了兩個(gè)版本,經(jīng)過大量在 Github 上進(jìn)行的冗長(zhǎng)討論[8],Go 團(tuán)隊(duì)認(rèn)為在通用代碼中使用接口作為 comparable 類型應(yīng)該是足夠安全的。如果你在 Go 1.20 中運(yùn)行 keys(map[any]any{}) ,它可以正常運(yùn)行,你不必考慮上面的任何說明。
Go 1.20 中的另一個(gè)語言變化更容易解釋。如果你有一個(gè)切片,現(xiàn)在你可以很容易地將其轉(zhuǎn)換為一個(gè)固定長(zhǎng)度的數(shù)組:
s := []string{"a", "b", "c"}
a := [3]string(s)
如果切片比數(shù)組短,你會(huì)因越界而產(chǎn)生 panic:
s := []int{1, 2, 3}
a := [4]int(s)
// panic: 運(yùn)行時(shí)錯(cuò)誤: 不能將長(zhǎng)度為 3 的切片轉(zhuǎn)換成長(zhǎng)度為 4 的數(shù)組或數(shù)組指針
這源于 Go 1.17 中增加的數(shù)組指針轉(zhuǎn)換特性:
s := []string{"a", "b", "c"}
p := (*[3]string)(s)
在這種情況下,p 指向 s 定義的數(shù)組,因此修改一個(gè)就會(huì)修改另一個(gè):
s := []string{"a", "b", "c"}
p := (*[3]string)(s)
s[0] = "d"
p[1] = "e"
fmt.Println(s, p) // [d e c] &[d e c]
另一方面,隨著 Go 1.20 中新增的切片轉(zhuǎn)換為數(shù)組特性,數(shù)組是 切片內(nèi)容的副本:
s := []string{"a", "b", "c"}
a := [3]string(s)
s[0] = "d"
a[1] = "e"
fmt.Println(s, a)
// [d b c] [a e c]
除了將切片轉(zhuǎn)換為數(shù)組的語法外,Go 1.20 還為處理切片數(shù)據(jù)的 unsafe 包帶來了一些新增內(nèi)容。
reflect 包一直有 reflect.SliceHeader[9] 和 reflect.StringHeader[10] ,它們是 Go 中切片和字符串的運(yùn)行時(shí)表示:
type SliceHeader struct {
Data uintptr
Len int
Cap int
}
type StringHeader struct {
Data uintptr
Len int
}
reflect.SliceHeader 和 reflect.StringHeader 都有一個(gè) Warning 提示:“它的表示方法可能在以后的版本中改變,因此不能確保障安全或可移植”,并且在試圖廢除它們[11]。誤用這些類型可能會(huì)導(dǎo)致代碼崩潰[12],但是在實(shí)踐中,很多程序都依賴于類似這樣的切片布局,很難想象 Go 團(tuán)隊(duì)會(huì)在沒有大量警告的情況下改變它,因?yàn)楹芏喑绦驎?huì)崩潰。
為了給 Gopher 們提供一種官方支持的編寫不安全代碼的方式,Go 1.17 增加了unsafe.Slice[13],它允許你把任何指針變成一個(gè)切片(不管是否是個(gè)好主意)。
obj := struct{ x, y, z int }{1, 2, 3}
slice := unsafe.Slice(&obj.x, 3)
obj.x = 4
slice[1] = 5
fmt.Println(obj, slice)
// {4 5 3} [4 5 3]
在 Go 1.20 中,還有 unsafe.SliceData[14](它返回一個(gè)指向切片數(shù)據(jù)的指針),unsafe.String[15](它以不安全的方式通過一個(gè) byte 指針創(chuàng)建字符串),以及 unsafe.StringData[16](它以不安全的方式返回一個(gè)指向字符串?dāng)?shù)據(jù)的指針)。
這些字符串函數(shù)是額外增加的不安全方式,因?yàn)樗鼈冊(cè)试S你違反 Go 的字符串不可變規(guī)則,但它也給了你很大的控制權(quán),可以在不分配新內(nèi)存的前提下轉(zhuǎn)換 byte 切片。
這些工具像利刃一樣,好用卻很容易割傷自己。在語言中直接支持這些工具可能更好,而不是僅僅讓大家使用 unsafe.Pointer 來祈禱它奏效。
用 Hank Hill 的話來形容,“無論你做什么,你都應(yīng)該以正確的方式去做,即使是錯(cuò)誤的事情。[17]”
[1] https://blog.carlmjohnson.net/post/2021/golang-118-minor-features/
[2] https://blog.carlmjohnson.net/post/2022/golang-118-even-more-minor-features/
[3] https://blog.carlmjohnson.net/post/2022/golang-119-new-features/
[4] https://groups.google.com/g/golang-nuts/c/HMUAm5j5raw/m/va3dxBFyAgAJ
[5] https://tip.golang.org/doc/go1.20
[6] https://github.com/golang/go/issues/49587
[7] https://github.com/golang/go/issues/50646
[8] https://github.com/golang/go/issues/51338
[9] https://pkg.go.dev/reflect#SliceHeader
[10] https://pkg.go.dev/reflect#StringHeader
[11] https://go-review.googlesource.com/c/go/+/401434
[12] https://github.com/golang/go/issues/40701
[13] https://pkg.go.dev/unsafe#Slice
[14] https://pkg.go.dev/[email protected]#SliceData
[15] https://pkg.go.dev/[email protected]#String
[16] https://pkg.go.dev/[email protected]#StringData
[17] https://www.getyarn.io/yarn-clip/08e52ddd-63ee-429b-b40c-b12c8ff6670b
https://blog.carlmjohnson.net/post/2023/golang-120-language-changes/
原文作者:Carl M. Johnson
本文永久鏈接: https://github.com/gocn/translator/blob/master/2023/w02_golang_120_language_changes.md 譯者 :pseudoyu校對(duì) :小超人
往期推薦
系統(tǒng)設(shè)計(jì)技巧:使用Postgres作為發(fā)布/訂閱和作業(yè)服務(wù)器
一文讀懂 Go Http Server 原理
想要了解Go更多內(nèi)容,歡迎掃描下方??關(guān)注公眾號(hào), 回復(fù)關(guān)鍵詞 [實(shí)戰(zhàn)群] ,就有機(jī)會(huì)進(jìn)群和我們進(jìn)行交流
分享、在看與點(diǎn)贊Go



