内置函数 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.Numd2.Num bigSlice[1000]猜猜如果d1和d2需要很长时间才能被释放会发生什么。答案是bigObj和bigSlice也会一直存在不被释放因为golang中结构体、数组/切片只要还有指针指向自己的字段或者元素那么整个结构体和数组/切片的内存都不能被释放。换句话说因为你的Data结构体持有了一个8字节的指针会导致它背后十几KB的内存一直没法释放尽管这些内存中的99%你完全用不到。这在比较宽泛的定义上已经属于是内存泄漏了。所以这时候帮助函数就起作用了。getPointerValue的参数不是指针因此会把传进来的值拷贝一份然后再取拷贝出来的新变量的指针这样就不会有指针指向那些大对象的字段或者元素了这些大对象也可以尽快得到释放从而不会浪费内存。背景故事到此结束到这里其实你也能猜出new被扩展的新功能大致是什么了。new在1.26中获得的新功能是可以接受一个表达式它会复制表达式的结果到同类型的变量里并返回指向这个变量的指针。看个例子new(1234) // *int, 指向的值是1234func getString() string {return apocelipes}new(getString()) // *string, 指向的值是apocelipess : 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 : tmpp2 : new(12345)// 改写成// var tmp int// tmp copy 12345// p2 : tmp可以看到new是先在当前作用域里创建一个临时变量然后再把表达式的结果复制进去的。全程没有其他的函数调用。