当前位置:  开发笔记 > 编程语言 > 正文

如何在Go中有效地连接字符串?

如何解决《如何在Go中有效地连接字符串?》经验,为你挑选了11个好方法。

在Go中,a string是原始类型,这意味着它是只读的,并且对它的每次操作都将创建一个新字符串.

因此,如果我想在不知道结果字符串长度的情况下多次连接字符串,那么最好的方法是什么?

天真的方式是:

s := ""
for i := 0; i < 1000; i++ {
    s += getShortStringFromSomewhere()
}
return s

但这似乎不是很有效.



1> marketer..:

注释于2018年添加

从Go 1.10开始有一种strings.Builder类型,请查看此答案以获取更多详细信息.

201x之前的答案

最好的方法是使用bytes包.它有一种Buffer实现的类型io.Writer.

package main

import (
    "bytes"
    "fmt"
)

func main() {
    var buffer bytes.Buffer

    for i := 0; i < 1000; i++ {
        buffer.WriteString("a")
    }

    fmt.Println(buffer.String())
}

这是在O(n)时间内完成的.


是.Go中几乎所有东西都是Unicode清洁的.
非常快.在我的程序中做了一些天真的"+"字符串连续从3分钟到1.3*秒*.
而不是`buffer:= bytes.NewBufferString("")`,你可以做`var buffer bytes.Buffer`.你也不需要任何这些分号:).
而不是println(string(buffer.Bytes())); 使用可以只做println(buffer.String())
+1"O(n)时间"; 我认为发表更多这样的言论非常重要.
Go 1.10添加[strings.Builder](https://beta.golang.org/pkg/strings/#Builder),这就像bytes.Buffer,但当你的最终目标是一个字符串时更快.
它可能会失败,但是"错误总是为零"([同源](http://golang.org/pkg/bytes/#Buffer.WriteString)).在正常情况下,我认为你不必担心失败.
`WriteString`可以[失败](http://golang.org/pkg/bytes/#Buffer.WriteString).

2> cd1..:

连接字符串的最有效方法是使用内置函数copy.在我的测试中,这种方法比使用方法快3倍,比使用bytes.Buffer运算符快得多(~12,000x)+.此外,它使用更少的内存.

我已经创建了一个测试用例来证明这一点,结果如下:

BenchmarkConcat  1000000    64497 ns/op   502018 B/op   0 allocs/op
BenchmarkBuffer  100000000  15.5  ns/op   2 B/op        0 allocs/op
BenchmarkCopy    500000000  5.39  ns/op   0 B/op        0 allocs/op

以下是测试代码:

package main

import (
    "bytes"
    "strings"
    "testing"
)

func BenchmarkConcat(b *testing.B) {
    var str string
    for n := 0; n < b.N; n++ {
        str += "x"
    }
    b.StopTimer()

    if s := strings.Repeat("x", b.N); str != s {
        b.Errorf("unexpected result; got=%s, want=%s", str, s)
    }
}

func BenchmarkBuffer(b *testing.B) {
    var buffer bytes.Buffer
    for n := 0; n < b.N; n++ {
        buffer.WriteString("x")
    }
    b.StopTimer()

    if s := strings.Repeat("x", b.N); buffer.String() != s {
        b.Errorf("unexpected result; got=%s, want=%s", buffer.String(), s)
    }
}

func BenchmarkCopy(b *testing.B) {
    bs := make([]byte, b.N)
    bl := 0

    b.ResetTimer()
    for n := 0; n < b.N; n++ {
        bl += copy(bs[bl:], "x")
    }
    b.StopTimer()

    if s := strings.Repeat("x", b.N); string(bs) != s {
        b.Errorf("unexpected result; got=%s, want=%s", string(bs), s)
    }
}

// Go 1.10
func BenchmarkStringBuilder(b *testing.B) {
    var strBuilder strings.Builder

    b.ResetTimer()
    for n := 0; n < b.N; n++ {
        strBuilder.WriteString("x")
    }
    b.StopTimer()

    if s := strings.Repeat("x", b.N); strBuilder.String() != s {
        b.Errorf("unexpected result; got=%s, want=%s", strBuilder.String(), s)
    }
}


请注意,基准测试结果是扭曲的,不是真实的.将使用不同的`bN`值调用不同的基准函数,因此您不会比较要执行的同一任务的执行时间(例如,一个函数可能附加`1,000`字符串,另一个函数可能附加`10,000`例如,在"BenchmarkConcat()"中,这可以对1个追加的平均时间产生很大的影响.你应该在每种情况下都使用相同的追加计数(当然不是`bN`),并且在`for`范围内的所有连接中进行`bN`(即嵌入的2` for循环).
此外,通过明确忽略分配所花费的时间来扭曲复制基准,这包括在其他基准中.
bytes.Buffer应该与副本基本相同(我猜有一些额外的簿记),速度没有那么不同.所以我会用那个:).区别在于缓冲区以0字节开始,因此必须重新分配(这使得它看起来有点慢).但是更容易使用.
此外,复制基准依赖于知道结果字符串的长度.
`buffer.Write`(bytes)比`buffer.WriteString`快30%.[如果您可以将数据作为`[] byte`获取,则非常有用]
@ cd1请以@icza提到的方式修复你的答案.```bytes.Buffer```只比字符串连接"+"快100倍.
@Aktau这也是我首先想到的,然后我改变了基准函数`BenchmarkBuffer()`以创建一个具有足够后备数组的缓冲区(以避免重新分配),如下所示:`buffer:= bytes.NewBuffer(make([] byte) ,0,bN*10 + 100))`.测试结果或多或少相同!

3> Inanc Gumus..:

从Go 1.10开始strings.Builder,这里有一个.

Builder用于使用Write方法高效地构建字符串.它最小化了内存复制.零值可以使用.


用法:

它几乎一样bytes.Buffer.

package main

import (
    "strings"
    "fmt"
)

func main() {
    var str strings.Builder

    for i := 0; i < 1000; i++ {
        str.WriteString("a")
    }

    fmt.Println(str.String())
}

它支持的StringBuilder方法和接口:

它的方法是在考虑现有接口的情况下实现的,因此您可以在代码中轻松切换到新的Builder.

Grow(int)- > bytes.Buffer #Erow

Len()int - > bytes.Buffer #Len

Reset()- > bytes.Buffer #Reset

String()string - > fmt.Stringer

写([] byte)(int,error)- > io.Writer

WriteByte(byte)错误- > io.ByteWriter

WriteRune(rune)(int,error)- > bufio.Writer#WriteRune - bytes.Buffer#WriteRune

WriteString(string)(int,error)- > io.stringWriter


零值使用:

var buf strings.Builder

与bytes.Buffer的差异:

它只能增长或重置.

bytes.Buffer底层字节可以像这样逃避:(*Buffer).Bytes(); strings.Builder防止这个问题.

它还有一个copyCheck机制,可以防止意外复制它(io.Reader).


在这里查看其源代码.


"逃避"是什么意思?你的意思是字符串中的转义,或者只是可以暴露基础字节?

4> mbarkhau..:

字符串包中有一个库函数,名为Join:http: //golang.org/pkg/strings/#Join

看一下代码Join显示类似于Append函数的方法Kinopiko写道:https://golang.org/src/strings/strings.go#L420

用法:

import (
    "fmt";
    "strings";
)

func main() {
    s := []string{"this", "is", "a", "joined", "string\n"};
    fmt.Printf(strings.Join(s, " "));
}

$ ./test.bin
this is a joined string


当你必须循环不是[]字符串的东西时不起作用.

5> 小智..:

我只是在我自己的代码(递归树步行)中对上面发布的最佳答案进行了基准测试,而简单的concat运算符实际上比它更快BufferString.

func (r *record) String() string {
    buffer := bytes.NewBufferString("");
    fmt.Fprint(buffer,"(",r.name,"[")
    for i := 0; i < len(r.subs); i++ {
        fmt.Fprint(buffer,"\t",r.subs[i])
    }
    fmt.Fprint(buffer,"]",r.size,")\n")
    return buffer.String()
}

这需要0.81秒,而以下代码:

func (r *record) String() string {
    s := "(\"" + r.name + "\" ["
    for i := 0; i < len(r.subs); i++ {
        s += r.subs[i].String()
    }
    s += "] " + strconv.FormatInt(r.size,10) + ")\n"
    return s
} 

只花了0.61秒.这可能是由于创建新的开销BufferString.

更新:我还对该join功能进行了基准测试,并在0.54秒内运行.

func (r *record) String() string {
    var parts []string
    parts = append(parts, "(\"", r.name, "\" [" )
    for i := 0; i < len(r.subs); i++ {
        parts = append(parts, r.subs[i].String())
    }
    parts = append(parts, strconv.FormatInt(r.size,10), ")\n")
    return strings.Join(parts,"")
}


速度慢可能与使用fmt.Fprint而不是`buffer.WriteString("\ t");``buffer.WriteString(subs [i]);`有关.
我认为OP更关注内存复杂性而不是运行时复杂性,因为天真的字符串连接每次都会导致新的内存分配.

6> 小智..:

您可以创建一大块字节,并使用字符串切片将短字符串的字节复制到其中."Effective Go"中有一个功能:

func Append(slice, data[]byte) []byte {
    l := len(slice);
    if l + len(data) > cap(slice) { // reallocate
        // Allocate double what's needed, for future growth.
        newSlice := make([]byte, (l+len(data))*2);
        // Copy data (could use bytes.Copy()).
        for i, c := range slice {
            newSlice[i] = c
        }
        slice = newSlice;
    }
    slice = slice[0:l+len(data)];
    for i, c := range data {
        slice[l+i] = c
    }
    return slice;
}

然后,当操作完成时,使用string ( )大块字节将其再次转换为字符串.


在有效的过程中,它还表示这个想法非常有用,它在内置中被捕获.所以你可以用`append(slice,byte ...)`代替你的函数.

7> rog..:

这是最快的解决方案,不需要您首先了解或计算总体缓冲区大小:

var data []byte
for i := 0; i < 1000; i++ {
    data = append(data, getShortStringFromSomewhere()...)
}
return string(data)

根据我的基准测试,它比复制解决方案慢20%(每次追加8.1ns而不是6.72ns),但仍然比使用bytes.Buffer快55%.



8> 小智..:
package main

import (
  "fmt"
)

func main() {
    var str1 = "string1"
    var str2 = "string2"
    out := fmt.Sprintf("%s %s ",str1, str2)
    fmt.Println(out)
}


欢迎来到Stack Overflow!花点时间阅读帮助中心的[编辑帮助](http://stackoverflow.com/editing-help).Stack Overflow上的格式与其他站点不同.

9> PickBoy..:
更新2018-04-03

从Go 1.10开始,strings.Builder建议替代b.N.检查1.10发行说明

新类型的Builder是bytes.Buffer的替代,用于将文本累积到字符串结果中的用例.Builder的API是bytes.Buffer的受限子集,它允许它安全地避免在String方法期间制作数据的副本.

================================================== ==========

@ cd1和其他答案的基准代码是错误的.b.N不应该在基准函数中设置.它由go测试工具动态设置,以确定测试的执行时间是否稳定.

基准函数应该运行相同的测试CopyPreAllocate时间,并且循环内的测试对于每次迭代应该是相同的.所以我通过添加内循环来修复它.我还为其他一些解决方案添加了基准:

package main

import (
    "bytes"
    "strings"
    "testing"
)

const (
    sss = "xfoasneobfasieongasbg"
    cnt = 10000
)

var (
    bbb      = []byte(sss)
    expected = strings.Repeat(sss, cnt)
)

func BenchmarkCopyPreAllocate(b *testing.B) {
    var result string
    for n := 0; n < b.N; n++ {
        bs := make([]byte, cnt*len(sss))
        bl := 0
        for i := 0; i < cnt; i++ {
            bl += copy(bs[bl:], sss)
        }
        result = string(bs)
    }
    b.StopTimer()
    if result != expected {
        b.Errorf("unexpected result; got=%s, want=%s", string(result), expected)
    }
}

func BenchmarkAppendPreAllocate(b *testing.B) {
    var result string
    for n := 0; n < b.N; n++ {
        data := make([]byte, 0, cnt*len(sss))
        for i := 0; i < cnt; i++ {
            data = append(data, sss...)
        }
        result = string(data)
    }
    b.StopTimer()
    if result != expected {
        b.Errorf("unexpected result; got=%s, want=%s", string(result), expected)
    }
}

func BenchmarkBufferPreAllocate(b *testing.B) {
    var result string
    for n := 0; n < b.N; n++ {
        buf := bytes.NewBuffer(make([]byte, 0, cnt*len(sss)))
        for i := 0; i < cnt; i++ {
            buf.WriteString(sss)
        }
        result = buf.String()
    }
    b.StopTimer()
    if result != expected {
        b.Errorf("unexpected result; got=%s, want=%s", string(result), expected)
    }
}

func BenchmarkCopy(b *testing.B) {
    var result string
    for n := 0; n < b.N; n++ {
        data := make([]byte, 0, 64) // same size as bootstrap array of bytes.Buffer
        for i := 0; i < cnt; i++ {
            off := len(data)
            if off+len(sss) > cap(data) {
                temp := make([]byte, 2*cap(data)+len(sss))
                copy(temp, data)
                data = temp
            }
            data = data[0 : off+len(sss)]
            copy(data[off:], sss)
        }
        result = string(data)
    }
    b.StopTimer()
    if result != expected {
        b.Errorf("unexpected result; got=%s, want=%s", string(result), expected)
    }
}

func BenchmarkAppend(b *testing.B) {
    var result string
    for n := 0; n < b.N; n++ {
        data := make([]byte, 0, 64)
        for i := 0; i < cnt; i++ {
            data = append(data, sss...)
        }
        result = string(data)
    }
    b.StopTimer()
    if result != expected {
        b.Errorf("unexpected result; got=%s, want=%s", string(result), expected)
    }
}

func BenchmarkBufferWrite(b *testing.B) {
    var result string
    for n := 0; n < b.N; n++ {
        var buf bytes.Buffer
        for i := 0; i < cnt; i++ {
            buf.Write(bbb)
        }
        result = buf.String()
    }
    b.StopTimer()
    if result != expected {
        b.Errorf("unexpected result; got=%s, want=%s", string(result), expected)
    }
}

func BenchmarkBufferWriteString(b *testing.B) {
    var result string
    for n := 0; n < b.N; n++ {
        var buf bytes.Buffer
        for i := 0; i < cnt; i++ {
            buf.WriteString(sss)
        }
        result = buf.String()
    }
    b.StopTimer()
    if result != expected {
        b.Errorf("unexpected result; got=%s, want=%s", string(result), expected)
    }
}

func BenchmarkConcat(b *testing.B) {
    var result string
    for n := 0; n < b.N; n++ {
        var str string
        for i := 0; i < cnt; i++ {
            str += sss
        }
        result = str
    }
    b.StopTimer()
    if result != expected {
        b.Errorf("unexpected result; got=%s, want=%s", string(result), expected)
    }
}

环境是OS X 10.11.6,2.2 GHz Intel Core i7

检测结果:

BenchmarkCopyPreAllocate-8         20000             84208 ns/op          425984 B/op          2 allocs/op
BenchmarkAppendPreAllocate-8       10000            102859 ns/op          425984 B/op          2 allocs/op
BenchmarkBufferPreAllocate-8       10000            166407 ns/op          426096 B/op          3 allocs/op
BenchmarkCopy-8                    10000            160923 ns/op          933152 B/op         13 allocs/op
BenchmarkAppend-8                  10000            175508 ns/op         1332096 B/op         24 allocs/op
BenchmarkBufferWrite-8             10000            239886 ns/op          933266 B/op         14 allocs/op
BenchmarkBufferWriteString-8       10000            236432 ns/op          933266 B/op         14 allocs/op
BenchmarkConcat-8                     10         105603419 ns/op        1086685168 B/op    10000 allocs/op

结论:

    AppendPreAllocate是最快的方式; Concat非常接近No.1,但编写代码更容易.

    Buffer#Write在速度和内存使用方面都表现不佳.不要使用它.

    Buffer#WriteStringstringDani-Br在评论中所说的相反,速度基本相同.考虑到Go []byte确实Copy存在,这是有道理的.

    bytes.Buffer基本上使用Copy与额外的簿记和其他东西相同的解决方案.

    AppendAppend使用64的引导程序大小,与bytes.Buffer相同

    Append使用更多的内存和分配,我认为它与它使用的增长算法有关.它不像bytes.Buffer那样快速增长内存

建议:

    对于OP想要的简单任务,我会使用AppendPreAllocatebytes.Buffer.它足够快且易于使用.

    如果需要同时读取和写入缓冲区,请使用strings.Builder.这就是它的设计目标.



10> Peter Buchma..:

我原来的建议是

s12 := fmt.Sprint(s1,s2)

但上面的回答使用bytes.Buffer - WriteString()是最有效的方法.

我的初步建议使用反射和类型切换.看,(p *pp) doPrint(p *pp) printArg
没有基本类型的通用Stringer()接口,正如我天真的想法.

至少,Sprint()内部使用bytes.Buffer.从而

`s12 := fmt.Sprint(s1,s2,s3,s4,...,s1000)`

在内存分配方面是可以接受的.

=> Sprint()连接可用于快速调试输出.
=>否则使用bytes.Buffer ... WriteString


它不是内置的,效率不高.

11> Peter Buchma..:

扩展cd1的答案:您可以使用append()而不是copy().append()会提供更大的预付款,花费更多的内存,但节省时间.我在你的顶部添加了两个基准测试.在本地运行

go test -bench=. -benchtime=100ms

在我的thinkpad T400s上它产生:

BenchmarkAppendEmpty    50000000         5.0 ns/op
BenchmarkAppendPrealloc 50000000         3.5 ns/op
BenchmarkCopy           20000000        10.2 ns/op

推荐阅读
手机用户2402851335
这个屌丝很懒,什么也没留下!
DevBox开发工具箱 | 专业的在线开发工具网站    京公网安备 11010802040832号  |  京ICP备19059560号-6
Copyright © 1998 - 2020 DevBox.CN. All Rights Reserved devBox.cn 开发工具箱 版权所有