好久沒有寫文章了,攢了一年的Golang版本特性的技術點以及踩過的坑,那就在新年第一篇的文章中做一個總結吧:
一、關于迭代器
(一)迭代器去掉了共享共享內存
一個經典的面試題
說到Golang經典的面試題,大家可能都刷到過很多,筆者這里也曾經收藏過幾個比較有意思的面試題,下面這個就是其中之一:
package mainimport "fmt"func main() {slice := []int{0, 1, 2, 3}mymap := make(map[int]*int)for index, value := range slice {mymap[index] = &value}for key, value := range mymap {fmt.Printf("map[%v]: %v\n", key, *value)}
}
上面的代碼輸出結果是什么?
看起來似乎比較簡單:
其實,在20230915時間之前,輸出的結果為:
map[3]=3
map[0]=3
map[1]=3
map[2]=3
因為for range
創建了迭代對象每個元素的副本,而不是直接返回每個元素的引用,如果使用該值變量的地址作為指向每個元素的指針,就會導致錯誤,在迭代時,返回的變量是同一個迭代過程中根據切片依次賦值的變量,所以最終map
中存儲的地址都是同一個變量的地址,而其值即為最后一次迭代中賦的值
是不是比較容易出錯
這里也確實讓廣大Golang開發者吐槽的地方,所以在golang1.22中,golang官方終于對這里出手了
可以看到這里官方自己也定義為最常見的Go錯誤之一
官方吐槽自己也是可以!
參考資料:
https://antonz.org/go-1-22/
https://tip.golang.org/doc/go1.22
(二)迭代器的"For" loops may 支持整數
一個更方便寫測試用例的小功能
for i := range 10 {fmt.Print(10 - i, " ")
}
fmt.Println()
fmt.Println("go1.22 has lift-off!")
這個在1.22中進行了添加
參考資料:
https://antonz.org/go-1-22/
https://blog.csdn.net/Wksycxy/article/details/136770738
https://tip.golang.org/ref/spec#For_range
(三)迭代器與range的結合
一致的迭代器形式
歷史背景:
Russ發現Go標準庫中有很多庫(如上截圖)中都有迭代器的實現,但形式不統一,沒有標準的“實現路徑”,各自為戰。這與Go面向工程的目標有悖,現狀阻礙了大型Go代碼庫中的代碼遷移。因此,Go團隊希望給大家帶來一致的迭代器形式,具體來說就是允許for range支持對一定類型函數值(function value)進行迭代,即range over func。
2024年2月,iterator以試驗特性被Go 1.22版本引入,通過GOEXPERIMENT=rangefunc可以開啟range-over-func特性以及使用iter包。
在golang.org/x/exp下面,Go團隊還提議維護一個xiter包,這個包內提供了用于組合iterator的基本適配器(adapter),不過目前該xiter包依舊處于proposal狀態,尚未落地。
2024年8月,iterator將伴隨Go 1.23版本正式落地,現在我們可以通過Go playground在線體驗iterator,當然你也可以安裝Go tip版本或Go 1.23的rc版在本地體驗。
終于在1.23落地了
這意味著很多用迭代器的地方可以通過range for的形式顯示調用
var m sync.Mapm.Store("alice", 11)
m.Store("bob", 12)
m.Store("cindy", 13)// 1.22
m.Range(func(key, value any) bool {fmt.Println(key, value)return true
})// 1.23
for key, val := range m.Range {fmt.Println(key, val)
}
可以看到后面這種方式更加的簡潔
還有一些比較簡潔的操作可以參考:
https://antonz.org/go-1-23/#timer-changes
二、關于切片
(一)增加了連接函數,順道在切片修改的函數做了健壯性處理
用好slices是golang開發的一個重要點
關于新的連接函數
s1 := []int{1, 2}
s2 := []int{3, 4}
s3 := []int{5, 6}
res := slices.Concat(s1, s2, s3)
fmt.Println(res)
關于delete函數
關于Compact和Replace函數
關于insert函數,更加的強Schema
如果參數 i 超出范圍,Insert函數會報panic。
參考資料:
https://antonz.org/go-1-22/
(二)關于slices.Repeat
用法比較簡單:
s := []int{1, 2}
r := slices.Repeat(s, 3)
fmt.Println(r)
輸出為:
1 2 1 2 1 2
三、關于隨機數
(一)新的隨機庫
1.22版本提供了一個新的庫math/rand/v2
math/rand/v2 并不是 math/rand的升級
這里可以說一說隨機數的歷史淵源
其實大家對math/rand不是那么滿意。
2017年,#20661 中提到math/rand.Read和crypto/rand.Read相近,導致本來應該使用crypto/rand.Read的地方使用了math/rand.Read,導致了安全問題
2017年,#21835 中 Rob Pike 提議在Go 2中使用PCG Source。
2018年,#26263 中 Josh Bleecher Snyder 提議對math/rand進行徹底的重構。
2023年6月, Russ Cox基于先前的對math/rand的吐槽,以及和Rob Pike的討論,建立了一個討論(#60751),準備新建一個包math/rand/v2,重新設計和實現一個新的偽隨機數的庫討論也很熱烈,最后實現了一個提案#61716,這個提案最直接的動機是清理 math/rand 并解決其中許多懸而未決的問題,特別是使用過時生成器、緩慢的算法,以及與 crypto/rand.Read 的不幸沖突。
由于go module的支持版本v2、v3、…, Go 1.22中將會有一個新的包math/rand/v2,這個包將會是一個新的包,而不是math/rand的升級版本。這個包的目標是提供一個更好的偽隨機數生成器,它的 API 也更加簡單易用,同時一些檢查工具也能支持這個包,不會報錯。
看樣子,math/rand/v2將會是第一個在標準庫中建立v2版本的包,如果大家能夠接受,將來會有更多的包加入進來,比如sync/v2、encoding/json/v2等等。
信息來源:https://colobu.com/2023/12/24/new-math-rand-in-Go/
可以看到當一個庫的問題足夠多的時候,總會有優秀的工程師站出來,進行一波大的重構,解決歷史問題,這個也適用于官方代碼
這里一個簡單的結論,關于隨技術
1.考慮到安全避免被人預測的場景下,要使用crypto/rand 包。
2.其他的情況一般來說使用ath/rand/v2就夠了
參考資料:
https://antonz.org/go-1-22/
https://colobu.com/2023/12/24/new-math-rand-in-Go/
四、關于HTTP庫
(一)增加不同方法的Handle調用及URL模糊匹配能力
如果使用Gin等框架的話,其實這個能力已經有了,現在Go1.22版本也提供
官方表示,我也可以主動提供一些框架的能力,如果大家有想寫Web框架的話會更友好
###1.下面這里如果填寫了POST,則使用專門handler來處理
mux.HandleFunc("POST /items/create", func(w http.ResponseWriter, r *http.Request) {fmt.Fprint(w, "POST item created")
})mux.HandleFunc("/items/create", func(w http.ResponseWriter, r *http.Request) {fmt.Fprint(w, "item created")
}){// uses POST routeresp, _ := http.Post(server.URL+"/items/create", "text/plain", nil)body, _ := io.ReadAll(resp.Body)fmt.Println("POST /items/create:", string(body))resp.Body.Close()
}{// uses generic routeresp, _ := http.Get(server.URL+"/items/create")body, _ := io.ReadAll(resp.Body)fmt.Println("GET /items/create:", string(body))resp.Body.Close()
}
輸出的結果為:
POST /items/create: POST item created
GET /items/create: item created
###2.下面這里可以使用像:/items/{id}通配符的形式獲取參數
mux.HandleFunc("/items/{id}", func(w http.ResponseWriter, r *http.Request) {id := r.PathValue("id")fmt.Fprintf(w, "Item ID = %s", id)
})req, _ := http.NewRequest("GET", server.URL+"/items/12345", nil)
resp, _ := http.DefaultClient.Do(req)
body, _ := io.ReadAll(resp.Body)
fmt.Println("GET /items/12345:", string(body))
resp.Body.Close()
以 …結尾的通配符(如 /files/{path…})必須出現在模式的末尾,也可以獲取
mux.HandleFunc("/files/{path...}", func(w http.ResponseWriter, r *http.Request) {path := r.PathValue("path")fmt.Fprintf(w, "File path = %s", path)
})req, _ := http.NewRequest("GET", server.URL+"/files/a/b/c", nil)
resp, _ := http.DefaultClient.Do(req)
body, _ := io.ReadAll(resp.Body)
fmt.Println("GET /files/a/b/c:", string(body))
resp.Body.Close()
以 / 結尾的模式會一如既往地匹配所有將其作為前綴的路徑。要匹配包括尾部斜杠的確切模式,請以 {KaTeX parse error: Expected 'EOF', got '}' at position 1: }? 結尾,如 /exact/ma…}:
mux.HandleFunc("/exact/match/{$}", func(w http.ResponseWriter, r *http.Request) {fmt.Fprint(w, "exact match")
})mux.HandleFunc("/exact/match/", func(w http.ResponseWriter, r *http.Request) {fmt.Fprint(w, "prefix match")
}){// exact matchreq, _ := http.NewRequest("GET", server.URL+"/exact/match/", nil)resp, _ := http.DefaultClient.Do(req)body, _ := io.ReadAll(resp.Body)fmt.Println("GET /exact/match/:", string(body))resp.Body.Close()
}{// prefix matchreq, _ := http.NewRequest("GET", server.URL+"/exact/match/123", nil)resp, _ := http.DefaultClient.Do(req)body, _ := io.ReadAll(resp.Body)fmt.Println("GET /exact/match/123:", string(body))resp.Body.Close()
}
這里有個細節:
**如果兩個模式在匹配的請求中重疊,則更具體的模式優先。**如果兩者都不更具體,則模式會發生沖突。此規則概括了原始優先規則,并維護了模式的注冊順序無關緊要的屬性。
###3.NewRequestWithContext
新的 NewRequestWithContext 方法創建具有上下文的傳入請求。
(二)增加Cookies的方法
如果使用Gin等框架的話,其實這個能力已經有了,現在Go1.23版本也提供
ParseCookie 函數
解析 Cookie 標頭值并返回在其中設置的所有 Cookie。由于相同的 Cookie 名稱可以多次出現,因此返回的 Value 可以包含給定鍵的多個值。
line := "session_id=abc123; dnt=1; lang=en; lang=de"
cookies, err := http.ParseCookie(line)
if err != nil {panic(err)
}
for _, cookie := range cookies {fmt.Printf("%s: %s\n", cookie.Name, cookie.Value)
}
ParseSetCookie 函數
解析 Set-Cookie 標頭值并返回 Cookie
line := "session_id=abc123; SameSite=None; Secure; Partitioned; Path=/; Domain=.example.com"
cookie, err := http.ParseSetCookie(line)
if err != nil {panic(err)
}
fmt.Println("Name:", cookie.Name)
fmt.Println("Value:", cookie.Value)
fmt.Println("Path:", cookie.Path)
fmt.Println("Domain:", cookie.Domain)
fmt.Println("Secure:", cookie.Secure)
fmt.Println("Partitioned:", cookie.Partitioned)
Request.CookiesNamed函數
檢索與給定名稱匹配的所有 Cookie
func handler(w http.ResponseWriter, r *http.Request) {cookies := r.CookiesNamed("session")if len(cookies) > 0 {fmt.Fprintf(w, "session cookie = %s", cookies[0].Value)} else {fmt.Fprint(w, "session cookie not found")}
}func main() {req := httptest.NewRequest("GET", "/", nil)req.AddCookie(&http.Cookie{Name: "session", Value: "abc123"})w := httptest.NewRecorder()handler(w, req)resp := w.Result()body, _ := io.ReadAll(resp.Body)fmt.Println(string(body))
}
五、關于計時器
(一)新的資源優化
1.23版本對計時器的資源使用做了優化
關于以下代碼
// go 1.22
type token struct{}func consumer(ctx context.Context, in <-chan token) {for {select {case <-in:// do stuffcase <-time.After(time.Hour):// log warningcase <-ctx.Done():return}}
}
會事實上導致廣義上的內存泄漏,即雖然有指針指向,但其實應用不在使用了
我們可以做個實驗:
編寫 100K 通道發送后的內存使用情況:
// go 1.22
func main() {ctx, cancel := context.WithCancel(context.Background())defer cancel()tokens := make(chan token)go consumer(ctx, tokens)memBefore := getAlloc()for range 100000 {tokens <- token{}}memAfter := getAlloc()memUsed := memAfter - memBeforefmt.Printf("Memory used: %d KB\n", memUsed/1024)
}
輸出結果為:
Memory used: 24325 KB
這是因為,time.After() 一旦調用了,該計時器在過期之前不會釋放,所以這里的內存都積壓了
而在 Go 1.23 中,不再被程序引用的 Timer和 Ticker立即符合垃圾回收的條件,即使它們的 Stop 方法尚未被調用。所以不會存在內存積壓問題
(二)關于reset的坑
看下面這段代碼:
// go 1.22
func main() {const timeout = 10 * time.Millisecondt := time.NewTimer(timeout)time.Sleep(20 * time.Millisecond)start := time.Now()t.Reset(timeout)<-t.Cfmt.Printf("Time elapsed: %dms\n", time.Since(start).Milliseconds())// expected: Time elapsed: 10ms// actual: Time elapsed: 0ms
}
這里輸出為:
Time elapsed: 0ms
因為計時器超時時間設置為 10 毫秒。所以在我們等待 20ms 之后,它已經過期并向 t.C 通道發送了一個值。由于Reset 不重置channel,因此 <-t.C 不會阻塞并立即進行。所以第9行會直接執行,從而打印的時間就是當時的時間(此外,由于 Reset函數重啟了計時器,在 10ms 后t.C還會收到一個過期的信號)
Go 1.23 中修復說明:
The timer channel associated with a Timer or Ticker is now unbuffered, with capacity 0. The main effect of this change is that Go now guarantees that for any call to a Reset or Stop method, no stale values prepared before that call will be sent or received after the call.
也就是說通過無緩沖的方式解決了通道重制的問題
詳細可以參考:
https://antonz.org/go-1-23/#timer-changes
六、關于“字符串駐留”
(一)unique 包
字符串去重
Go 1.23 標準庫引入了一個名為 unique 的新包,旨在實現可比較值的規范化。簡而言之,該包允許你對值進行去重,使其指向單個規范的唯一副本,并在底層有效管理這些規范副本
這里寫一個比較簡單的字符串駐留的函數
var internPool map[string]string// Intern 返回一個與 s 相等的字符串,但該字符串可能與之前傳遞給 Intern 的字符串共享存儲空間。
func Intern(s string) string {pooled, ok := internPool[s]if !ok {// 克隆字符串,以防它是某個更大的字符串的一部分。// 如果正確使用字符串駐留,這種情況應該很少見。pooled = strings.Clone(s)internPool[pooled] = pooled}return pooled
}
當你構建許多可能是重復的字符串時(例如在解析文本格式時),這非常有用。
此實現非常簡單,在某些情況下效果很好,但它也存在一些問題:
它永遠不會從池中刪除字符串。
多個 goroutine 無法安全地同時使用它。
它僅適用于字符串,即使該想法非常通用。
此實現還有一個錯失的機會,而且很微妙。在底層,字符串是由指針和長度組成的不可變結構。比較兩個字符串時,如果指針不相等,則必須比較它們的內容以確定是否相等。但如果我們知道兩個字符串是規范化的,那么只需檢查它們的指針就_足夠_了。
新的 unique 包引入了一個名為 Make 的函數,類似于 Intern
它的工作方式與 Intern 大致相同。在內部,它也使用全局映射(一個快速的泛型并發映射),Make 在該映射中查找提供的值。但它也與 Intern 有兩個重要區別。首先,它接受任何可比較類型的值。其次,它返回一個包裝值,即 HandleT,可以從中檢索規范值。
HandleT 是設計的關鍵。HandleT 具有以下屬性:當且僅當用于創建它們的_值_相等時,兩個 HandleT _值_才相等。更重要的是,比較兩個 HandleT 值的成本很低:它歸結為指針比較。與比較兩個長字符串相比,這要便宜一個數量級!
到目前為止,這在普通的 Go 代碼中都能做到。
關于使用
使用這里筆者找到了兩個例子,不過筆者覺得后續可以再找一些
// Addr 表示 IPv4 或 IPv6 地址(帶或不帶范圍尋址區域),類似于 net.IP 或 net.IPAddr。
type Addr struct {// 其他不相關的未導出字段...// 有關地址的詳細信息,匯總在一起并進行規范化。z unique.Handle[addrDetail]
}// addrDetail 指示地址是 IPv4 還是 IPv6,如果是 IPv6,則指定地址的區域名稱。
type addrDetail struct {isV6 bool // IPv4 為 false,IPv6 為 true。zoneV6 string // 如果 IsV6 為 true,則可能 != ""。
}var z6noz = unique.Make(addrDetail{isV6: true})// WithZone 返回一個與 ip 相同但具有提供的區域的 IP。如果區域為空,則刪除該區域。如果 ip 是 IPv4 地址,則 WithZone 是無操作的,并返回未更改的 ip。
func (ip Addr) WithZone(zone string) Addr {if !ip.Is6() {return ip}if zone == "" {ip.z = z6nozreturn ip}ip.z = unique.Make(addrDetail{isV6: true, zoneV6: zone})return ip
}
由于許多 IP 地址可能使用相同的區域,并且此區域是其身份的一部分,因此對它們進行規范化非常有意義。區域的去重減少了每個 netip.Addr 的平均內存占用量,而它們被規范化的事實意味著 netip.Addr 值的比較效率更高,因為比較區域名稱變成了簡單的指針比較。
具體可以參考:https://k8scat.com/posts/go/go-1.23-unique/
另一個例子:
假設我們有一個隨機詞生成器:
我們用它從 100 個單詞的詞匯表中生成 10,000 個單詞:
// wordGen returns a generator of random words of length wordLen
// from a set of nDistinct unique words.
func wordGen(nDistinct, wordLen int) func() string {vocab := make([]string, nDistinct)for i := range nDistinct {word := randomString(wordLen)vocab[i] = word}return func() string {word := vocab[rand.Intn(nDistinct)]return strings.Clone(word)}
}// randomString returns a random string of length n.
func randomString(n int) string {// omitted for brevity
}var words []stringfunc main() {const nWords = 10000const nDistinct = 100const wordLen = 40generate := wordGen(nDistinct, wordLen)memBefore := getAlloc()// store wordswords = make([]string, nWords)for i := range nWords {words[i] = generate()}memAfter := getAlloc()memUsed := memAfter - memBeforefmt.Printf("Memory used: %d KB\n", memUsed/1024)
}
運行結果為:
Memory used: 622 KB
而如果用unique
var words []unique.Handle[string]func main() {const nWords = 10000const nDistinct = 100const wordLen = 40generate := wordGen(nDistinct, wordLen)memBefore := getAlloc()// store word handleswords = make([]unique.Handle[string], nWords)for i := range nWords {words[i] = unique.Make(generate())}memAfter := getAlloc()memUsed := memAfter - memBeforefmt.Printf("Memory used: %d KB\n", memUsed/1024)
}
結果為:
Memory used: 96 KB
具體可以參考:https://antonz.org/go-1-23/#timer-changes