Go 1.26 內置函數 new 新特性
目前golang 1.26的各種新特性還在開發中,不過其中一個在開發完成之前就已經被官方拿到臺面上進行宣傳了——內置函數new功能擴展。
每個新特性其實都有它的背景故事,沒有需求的驅動也就不會有新特性的誕生。所以在介紹這個新特性之前我們先來了解下是什么樣的場景催生了這個功能。
如果你經常瀏覽一些大型的go項目,尤其是那些需要頻繁和JSON、GRPC或者yaml打交道的項目,比如k8s,你會發現這些代碼庫會提供一些和下面代碼類似的幫助函數:
func getPointerValue[T any](v T) *T {
return &v
}
這個是我用泛型改寫的,代碼庫里通常都是getIntPointerValue(int) *int這樣非泛型函數。函數的作用很簡單,返回指向自己參數的指針。但這樣簡單的三行代碼有什么用呢?
用處有好幾個,第一個是在json或者rpc里有時候我們會用指針的nil來表示這個值沒有生效,和字段類型的零值做區分,但這使得給字段賦值變麻煩了:
type Data struct {
Num *uint
}
d := &Data{}
d.Num = &12345 // 編譯錯誤
d.Num = getPointerValue(12345)
這行代碼d.Num = &12345是語法錯誤,因為在golang里規定不能對字面量以及常量取地址。不僅如此,類似d.Num = &getNum()這樣的代碼也是無法編譯的,因為go也規定了不能對右值取地址。
如果沒有幫助函數,我們需要用一個中間變量接住這些值,然后再把這個中間變量的指針賦值給結構體的字段。
第二個作用在于防止潛在的內存泄漏:
type BigStruct struct {
// 100個其他字段
Num int
}
bigObj := &BigStruct{....}
bigSlice := make([]int, 1024)
d1.Num = &bigObj.Num
d2.Num = &bigSlice[1000]
猜猜如果d1和d2需要很長時間才能被釋放會發生什么。答案是bigObj和bigSlice也會一直存在不被釋放,因為golang中結構體、數組/切片只要還有指針指向自己的字段或者元素,那么整個結構體和數組/切片的內存都不能被釋放。換句話說因為你的Data結構體持有了一個8字節的指針,會導致它背后十幾KB的內存一直沒法釋放,盡管這些內存中的99%你完全用不到。這在比較寬泛的定義上已經屬于是內存泄漏了。
所以這時候幫助函數就起作用了。getPointerValue的參數不是指針,因此會把傳進來的值拷貝一份,然后再取拷貝出來的新變量的指針,這樣就不會有指針指向那些大對象的字段或者元素了,這些大對象也可以盡快得到釋放從而不會浪費內存。
背景故事到此結束,到這里其實你也能猜出new被擴展的新功能大致是什么了。
new在1.26中獲得的新功能是可以接受一個表達式,它會復制表達式的結果到同類型的變量里并返回指向這個變量的指針。
看個例子:
new(1234) // *int, 指向的值是1234
func getString() string {
return "apocelipes"
}
new(getString()) // *string, 指向的值是"apocelipes"
s := "Hello, "
new(s + getString() + "!") // *string, 指向的值是表達式的結果"Hello, apocelipes!"
功能很簡單,相當于把上面的幫助函數getPointerValue集成到了現有的內置函數new里。這能讓我們簡化一些代碼。
不過按照go團隊以往的做法,如果只是簡化代碼的話其實是不會在原有的內置函數上新增功能的。現在這么做了說明還有額外的好處——性能。
我們看個性能測試:
func BenchmarkOld(b *testing.B) {
for b.Loop() {
p := getPointerValue(123)
if p == nil || *p != 123 {
b.Fatal()
}
}
}
func BenchmarkNew(b *testing.B) {
for b.Loop() {
p := new(123)
if p == nil || *p != 123 {
b.Fatal()
}
}
}
這段代碼需要master分支上的go編譯器才能正常編譯運行,我使用的版本是go1.26-devel_d7a38adf4c。
結果:

可以看到使用幫助函數要額外多分配一次內存,速度也更慢。這是因為golang的逃逸分析主要保證內存安全,而在優化上比較保守,所以在處理我們的幫助函數時哪怕這個函數已經被內聯,編譯器還是會選擇分配一塊堆內存再返回指向這塊內存的指針。換句話說,編譯器不夠“聰明”。
但內置函數就不一樣了,內置函數是被編譯器特殊處理的,new會被編譯器改寫:
p1 := new(int)
// 改寫成
// var tmp int
// p1 := &tmp
p2 := new(12345)
// 改寫成
// var tmp int
// tmp = copy 12345
// p2 := &tmp
可以看到new是先在當前作用域里創建一個臨時變量,然后再把表達式的結果復制進去的。全程沒有其他的函數調用。
對于改寫后的代碼,逃逸分析有充足的信息來決定改寫產生的tmp應該分配在棧上還是堆上,比起幫助函數來說獲得了更多的優化機會,因此性能也更好。
所以官方才有底氣提前宣傳,畢竟不僅解決了痛點,還有額外的收獲。
總結
1.26開始內置函數new的參數除了能接受一個類型名稱,現在還可以接收任意的表達式了。
在新版本中我們可以直接利用內置函數new不需要寫幫助函數了,同時還能收獲更高的性能。
當然,1.26的新特性開發窗口還沒結束,不能保證最終發布的功能和文章里介紹的一模一樣,但看官方這架勢這個新特性大概率是板上釘釘了,先用這篇文章嘗個鮮也未嘗不可。


浙公網安備 33010602011771號