如何在Go语言中(深度)复制字符串?

4xrmg8kj  于 2022-12-07  发布在  Go
关注(0)|答案(5)|浏览(659)

我应该先解释一下为什么我想要那个。
我知道在Go语言中,substring(s[i:j])和string.Split以及其他一些字符串操作可以 * 就地 * 工作:所得子串共享原始串的相同存储器块。
例如我读取一个大字符串,解析并从中得到一些子字符串,这些子字符串将长期保存在服务器程序中,它们将“占用”GC的大内存块,浪费内存。我假设如果我可以复制这些子字符串并保留这些副本,GC就可以释放那个大字符串。
但是我在Go语言中找不到字符串复制机制,我试着把它转换成[]byte,然后再转换成string,在我的特定用例中,内存使用量下降了大约3/4。
但这感觉不对:第一,它引入了两个复制操作。第二,因为我从来没有真正写那个字节片,我怀疑它可能在发布版本中得到了优化。
我无法想象以前没有人问过这个问题,但是我的搜索没有产生任何相关的结果,或者在Go语言中有什么更好的实践来做这类事情吗?
顺便说一句,我试图附加一个空字符串(+"")到它,内存消耗没有下降,我假设它得到了优化,即使在测试构建。
为了测量内存使用情况,我调用了runtime.GC(),然后调用了runtime.ReadMemStats(),并比较了MemStats.Alloc,这在我的测试中似乎非常一致。

4bbkushb

4bbkushb1#

替代方案是,从go 1.20(2022年第四季度)开始:

sCopy := strings.Clone(s)

它来自问题4020045038,并从CL(更改列表)334884345849开始

字节,字符串:添加克隆

直接使用[]bytestring是常见的,并且经常需要复制它们。
此更改为字符串和字节添加了一个Clone帮助器,以满足此需求。
此外,还添加了一个性能指标评测,以提供证据来说明为什么要使用copy来实现bytes.Clone

字符串:添加克隆功能

新的strings.Clone函数复制输入字符串,但不复制引用输入字符串内存的返回克隆字符串
请参阅strings/clone.go

// 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 {
    if len(s) == 0 {
        return ""
    }
    b := make([]byte, len(s))
    copy(b, s)
    return unsafe.String(&b[0], len(b))
}
lqfhib0f

lqfhib0f2#

Go语言中的字符串一旦创建就不可变。
我更喜欢下面的构建器。你继续添加到构建器的缓冲区(可变)WriteString,一旦完成调用String方法,这是返回指针,而不是缓冲区切片的另一个副本。

somestring := "Hello Go"
    var sb strings.Builder
    if _, err := sb.WriteString(somestring); err != nil {
        //log & return
    }
    newstring := sb.String()

从go源代码检查构建器String()的实现。它返回指针并转换为 *string。没有第二个副本。

// String returns the accumulated string.
func (b *Builder) String() string {
    return *(*string)(unsafe.Pointer(&b.buf))
}
flvlnr44

flvlnr443#

字符串作为指向基础字节数组和字符串长度的指针来实现。当从现有字符串创建切片时,新字符串仍然指向基础数组,可能指向该数组中具有不同长度的不同偏移量。这样,许多小字符串就可以使用单个基础大数组。
正如您所提到的,如果您有一个大字符串,并且您解析它以获得较小的字符串,您最终会将这个大字符串保留在内存中,因为GC只知道底层数组和指向它的指针。有两种方法可以处理这个问题:

  • 不用一个大的字符串,而是保留一个[]byte或者使用一个基于字节流的读取器/扫描器,并且在解析时从输入中创建字符串。这样GC在解析完成时将收集底层的[]byte,并且您将得到没有底层大块的字符串。
  • 执行您已经描述的操作,使用string([]byte(s[x:y]))copy深度复制字符串。
a14dhokn

a14dhokn4#

使用以下函数深层复制字符串:

func deepCopy(s string) string {
    b := make([]byte, len(s))
    copy(b, s)
    return *(*string)(unsafe.Pointer(&b))
}

该函数将数据复制到新分配的字节切片中。该函数使用unsafe包将切片标头转换为字符串标头,而不复制字节。
如果直接使用不安全的包是一个问题,那么使用strings.Builder。strings.Builder类型在幕后执行不安全的恶作剧。

func deepCopy(s string) string {
     var sb strings.Builder
     sb.WriteString(s)
     return sb.String()
 }

不需要检查sb.WriteString返回的错误。Builder.WriteString方法返回错误是为了使Builder类型满足io.StringWriter接口,而不是因为WriteString可以返回非空错误。

dhxwm5r4

dhxwm5r45#

另一种克隆字符串的方法是通过与非空字符串连接来创建一个新的字符串,这将分配一个新的底层缓冲区,然后获取结果的一个子字符串:

cloned := (s + ".")[:len(s)]

相关问题