Go1.18 快讯:新增字符串 Clone API
大家好,我是 polarisxu。
Go 1.18 虽然还有 4 个月发布,但大部分的功能基本确定。我们可以提前知晓、熟悉。
今天介绍的是标准库中新增的一个 API:strings.Clone()
。
从名称可以知道,这是克隆。很多其他语言一开始就有这样的功能。比如 PHP 有 clone 关键字、__clone
魔术方法;Java 的根类 Object 有 clone 方法等。
01 函数签名
该函数的定义如下(见:https://pkg.go.dev/strings@master#Clone)
// Clone returns a fresh copy of s.
// It guarantees to make a copy of s into a new allocation,
// which can be important when retaining only a small substring
// of a much larger string. Using Clone can help such programs
// use less memory. Of course, since using Clone makes a copy,
// overuse of Clone can make programs use more memory.
// Clone should typically be used only rarely, and only when
// profiling indicates that it is needed.
// For strings of length zero the string "" will be returned
// and no allocation is made.
func Clone(s string) string
Clone 返回 s 的新副本。它保证将 s 复制到一个新分配的副本中,当只保留一个很大的字符串中的一个小子字符串时,这一点很重要。使用克隆可以帮助这些程序使用更少的内存。当然,由于使用克隆制作拷贝,过度使用克隆会使程序使用更多内存。通常,只有在分析表明需要克隆时,才谨慎使用克隆。对于长度为零的字符串,将返回字符串 ""
,不进行内存分配。
02 举例说明
大家可能还是迷惑,不知道有啥用。举一个代码例子说明:
package main
import (
"fmt"
"reflect"
"unsafe"
)
func main() {
s := "abcdefghijklmn"
s1 := s[:4]
sHeader := (*reflect.StringHeader)(unsafe.Pointer(&s))
s1Header := (*reflect.StringHeader)(unsafe.Pointer(&s1))
fmt.Println(sHeader.Len == s1Header.Len)
fmt.Println(sHeader.Data == s1Header.Data)
// Output:
// false
// true
}
Len 不相等不需要解释,Data 相等就值得注意。
上面代码,有些人可能不知道什么意思。这里涉及到 Go 中 string 类型的底层结构。在 Go 中,string 类型的底层表示如下:
type string struct { ptr unsafe.Pointer len int }
而 reflect.StringHeader 结构是对字符串底层结构的反射表示。
在上面示例场景中,如果 s 很大,而之后我们只需要使用它的某个短子串,这会导致内存的浪费,因为子串和原字符串的 Data 部分指向相同的内存,因此整个字符串并不会被 GC 回收。
strings.Clone
函数就是为了解决这个问题的:(要正常运行下面代码,需要按照 Go tip 版本)
s2 := strings.Clone(s[:4])
s2Header := (*reflect.StringHeader)(unsafe.Pointer(&s2))
fmt.Println(sHeader.Len == s2Header.Len)
fmt.Println(sHeader.Data == s2Header.Data)
// Output:
// false
// false
通过克隆得到 s2,从最后输出结果看,Data 已经不同了,原始的长字符串就可以被垃圾回收了。(你也可以将传递给 Clone 的参数改为 s1,后面部分用 s1 和 s2 比)
03 内部实现
知道了克隆的用途,再看看 strings.Clone 的实现。
func Clone(s string) string {
if len(s) == 0 {
return ""
}
b := make([]byte, len(s))
copy(b, s)
return *(*string)(unsafe.Pointer(&b))
}
这里有两个关键点:
- 通过 copy 进行拷贝。其实普通的 slice,也会有需要克隆的场景,这时,需要我们手动执行 copy 操作。
- return 后面的语句
*(*string)(unsafe.Pointer(&b))
,实现 []byte 到 string 的零内存拷贝转换。
04 总结
Go 虽然有 GC,大部分时候不需要考虑内存问题,但对内存的使用,我们需要有敬畏之心,特别是大块内存、重复分配内存的场景,我们需要知晓如何优化,写出真正高质量的代码。
strings.Clone 的使用很简单,但希望通过本文,你在写 Go 代码时,对类似场景下,slice 的正确使用有启发(string 可以认为是特殊的 slice)。