在 Go 中,string
是一种原始类型,这意味着它是只读的,并且对它的每次操作都会创建一个新字符串。
因此,如果我想在不知道结果字符串长度的情况下多次连接字符串,那么最好的方法是什么?
天真的方法是:
var s string
for i := 0; i < 1000; i++ {
s += getShortStringFromSomewhere()
}
return s
但这似乎不是很有效。
append()
进入该语言之前编写的,这是一个很好的解决方案。它会像 copy()
一样快速执行,但会首先增长切片,即使这意味着如果容量不足则分配一个新的支持数组。如果您想要它的额外便利方法或者您使用的包需要它,bytes.Buffer
仍然有意义。
1 + 2 + 3 + 4 + ...
。它是 n*(n+1)/2
,底边为 n
的三角形的面积。当您在循环中附加不可变字符串时,您分配大小 1,然后是大小 2,然后是大小 3,等等。这种二次方的资源消耗表现在更多方面。
新的方法:
从 Go 1.10 开始,有一个 strings.Builder
类型,please take a look at this answer for more detail。
旧方式:
使用 bytes
包。它有一个实现 io.Writer
的 Buffer
类型。
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 1.10+ 中有 strings.Builder
、here。
Builder 用于使用 Write 方法有效地构建字符串。它最大限度地减少了内存复制。零值可以使用了。
例子
bytes.Buffer
几乎相同。
package main
import (
"strings"
"fmt"
)
func main() {
// ZERO-VALUE:
//
// It's ready to use from the get-go.
// You don't need to initialize it.
var sb strings.Builder
for i := 0; i < 1000; i++ {
sb.WriteString("a")
}
fmt.Println(sb.String())
}
Click to see this on the playground。
支持的接口
StringBuilder 的方法是在考虑现有接口的情况下实现的。这样您就可以在代码中轻松切换到新的 Builder 类型。
增长(int)-> bytes.Buffer#Grow
Len() int -> bytes.Buffer#Len
Reset() -> bytes.Buffer#Reset
String() 字符串 -> fmt.Stringer
Write([]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
与 bytes.Buffer 的区别
它只能增长或重置。
它有一个内置的 copyCheck 机制,可以防止意外复制它: func (b *Builder) copyCheck() { ... }
在 bytes.Buffer 中,可以像这样访问底层字节:(*Buffer).Bytes()。 strings.Builder 可以防止这个问题。有时,这不是问题,而是需要的。例如:对于将字节传递给 io.Reader 等时的偷看行为。
strings.Builder 可以防止这个问题。
有时,这不是问题,而是需要的。
例如:对于将字节传递给 io.Reader 等时的偷看行为。
bytes.Buffer.Reset() 倒带并重用底层缓冲区,而 strings.Builder.Reset() 不会,它会分离缓冲区。
笔记
不要复制 StringBuilder 值,因为它会缓存基础数据。
如果要共享 StringBuilder 值,请使用指向它的指针。
查看其源代码了解更多详情,here。
strings.Builder
使用指针接收器来实现它的方法,这让我很震惊。因此,我可能会使用 new
创建一个。
strings.Builder.Reset()
将底层切片设置为 nil
(无内存重用)。其中 bytes.Buffer.Reset()
将 []bytes
设置为零长度,保持分配的基础数组。当在 sync.Pool
中重用 strings.Builder
时,这让我有点吃惊,这似乎完全没用。
如果您知道要预分配的字符串的总长度,那么连接字符串的最有效方法可能是使用内置函数 copy
。如果您事先不知道总长度,请不要使用 copy
,而是阅读其他答案。
在我的测试中,这种方法比使用 bytes.Buffer
快约 3 倍,比使用运算符 +
快得多(约 12,000 倍)。此外,它使用更少的内存。
我创建了 a test case 来证明这一点,结果如下:
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)
}
}
buffer.Write
(字节)比 buffer.WriteString
快 30%。 [如果您可以将数据作为 []byte
获取,则很有用]
b.N
值调用不同的基准函数,因此您不会比较要执行的同一任务的执行时间(例如,一个函数可能附加 1,000
字符串,另一个可能附加 10,000
这会对 1 个追加的平均时间产生很大的影响,例如在 BenchmarkConcat()
中)。您应该在每种情况下使用相同的附加计数(当然不是 b.N
),并在 for
的主体内进行所有连接,范围为 b.N
(即嵌入 2 个 for
循环)。
如果您有一个想要有效地转换为字符串的字符串切片,那么您可以使用这种方法。否则,请查看其他答案。
字符串包中有一个名为 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
我刚刚在我自己的代码(递归树遍历)中对上面发布的最佳答案进行了基准测试,简单的 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,"")
}
buffer.WriteString("\t");
buffer.WriteString(subs[i]);
有关
package main
import (
"fmt"
)
func main() {
var str1 = "string1"
var str2 = "string2"
out := fmt.Sprintf("%s %s ",str1, str2)
fmt.Println(out)
}
您可以创建一大片字节并使用字符串切片将短字符串的字节复制到其中。 “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...)
替换你的函数,看起来。
这是不需要您首先知道或计算整体缓冲区大小的最快解决方案:
var data []byte
for i := 0; i < 1000; i++ {
data = append(data, getShortStringFromSomewhere()...)
}
return string(data)
通过我的 benchmark,它比复制解决方案慢 20%(每次追加 8.1ns 而不是 6.72ns),但仍比使用 bytes.Buffer 快 55%。
2018 年添加的注释
从 Go 1.10 开始,有一个 strings.Builder
类型,please take a look at this answer for more detail。
201x 之前的答案
@cd1 的基准代码和其他答案是错误的。 b.N
不应在基准函数中设置。由 go test 工具动态设置,判断测试的执行时间是否稳定。
基准函数应该运行相同的测试 b.N
次,并且循环内的测试对于每次迭代都应该是相同的。所以我通过添加一个内部循环来修复它。我还为其他一些解决方案添加了基准:
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
结论:
CopyPreAllocate 是最快的方法; AppendPreAllocate 非常接近 No.1,但更容易编写代码。 Concat 在速度和内存使用方面的表现都非常差。不要使用它。 Buffer#Write 和 Buffer#WriteString 在速度上基本相同,这与@Dani-Br 在评论中所说的相反。考虑到字符串在 Go 中确实是 []byte,这是有道理的。 bytes.Buffer 基本上使用与 Copy 相同的解决方案,并带有额外的簿记和其他内容。 Copy 和 Append 使用 64 的引导大小,与 bytes 相同。Buffer Append 使用更多的内存和分配,我认为这与它使用的增长算法有关。它的内存增长速度不如 bytes.Buffer
建议:
对于诸如 OP 想要的简单任务,我会使用 Append 或 AppendPreAllocate。它足够快且易于使用。如果需要同时读取和写入缓冲区,当然使用 bytes.Buffer 。这就是它的设计目的。
我原来的建议是
s12 := fmt.Sprint(s1,s2)
但是上面使用 bytes.Buffer - WriteString() 的答案是最有效的方法。
我最初的建议使用反射和类型切换。 See (p *pp) doPrint
and (p *pp) printArg
正如我天真地认为的那样,基本类型没有通用的 Stringer() 接口。
至少,Sprint() 在内部使用了 bytes.Buffer。因此
`s12 := fmt.Sprint(s1,s2,s3,s4,...,s1000)`
在内存分配方面是可以接受的。
=> Sprint() 连接可用于快速调试输出。 => 否则使用 bytes.Buffer ... WriteString
扩展 cd1 的答案:您可以使用 append() 而不是 copy()。 append() 进行了更大的预先准备,花费了更多的内存,但节省了时间。我在您的顶部添加了 two more benchmarks。在本地运行
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
这是@cd1 (Go 1.8
, linux x86_64
) 提供的基准测试的实际版本,修复了@icza 和@PickBoy 提到的错误。
Bytes.Buffer
仅比通过 +
运算符直接连接字符串快 7
倍。
package performance_test
import (
"bytes"
"fmt"
"testing"
)
const (
concatSteps = 100
)
func BenchmarkConcat(b *testing.B) {
for n := 0; n < b.N; n++ {
var str string
for i := 0; i < concatSteps; i++ {
str += "x"
}
}
}
func BenchmarkBuffer(b *testing.B) {
for n := 0; n < b.N; n++ {
var buffer bytes.Buffer
for i := 0; i < concatSteps; i++ {
buffer.WriteString("x")
}
}
}
时间:
BenchmarkConcat-4 300000 6869 ns/op
BenchmarkBuffer-4 1000000 1186 ns/op
b.N
是公共变量?
b.N
动态,您将在不同的测试用例中得到不同长度的字符串。请参阅comment
func JoinBetween(in []string, separator string, startIndex, endIndex int) string {
if in == nil {
return ""
}
noOfItems := endIndex - startIndex
if noOfItems <= 0 {
return EMPTY
}
var builder strings.Builder
for i := startIndex; i < endIndex; i++ {
if i > startIndex {
builder.WriteString(separator)
}
builder.WriteString(in[i])
}
return builder.String()
}
我使用以下方法:-
package main
import (
"fmt"
"strings"
)
func main (){
concatenation:= strings.Join([]string{"a","b","c"},"") //where second parameter is a separator.
fmt.Println(concatenation) //abc
}
package main
import (
"fmt"
)
func main() {
var str1 = "string1"
var str2 = "string2"
result := make([]byte, 0)
result = append(result, []byte(str1)...)
result = append(result, []byte(str2)...)
result = append(result, []byte(str1)...)
result = append(result, []byte(str2)...)
fmt.Println(string(result))
}
简单易消化的解决方案。评论中的详细信息。复制覆盖切片的元素。我们正在切片单个元素并覆盖它。
package main
import (
"fmt"
)
var N int = 100000
func main() {
slice1 := make([]rune, N, N)
//Efficient with fast performance, Need pre-allocated memory
//We can add a check if we reached the limit then increase capacity
//using append, but would be fined for data copying to new array. Also append happens after the length of current slice.
for i := 0; i < N; i++ {
copy(slice1[i:i+1], []rune{'N'})
}
fmt.Println(slice1)
//Simple but fast solution, Every time the slice capacity is reached we get a fine of effort that goes
//in copying data to new array
slice2 := []rune{}
for i := 0; i <= N; i++ {
slice2 = append(slice2, 'N')
}
fmt.Println(slice2)
}
带有内存分配统计信息的基准测试结果。检查 github 处的基准代码。
使用 strings.Builder 优化性能。
go test -bench . -benchmem
goos: darwin
goarch: amd64
pkg: github.com/hechen0/goexp/exps
BenchmarkConcat-8 1000000 60213 ns/op 503992 B/op 1 allocs/op
BenchmarkBuffer-8 100000000 11.3 ns/op 2 B/op 0 allocs/op
BenchmarkCopy-8 300000000 4.76 ns/op 0 B/op 0 allocs/op
BenchmarkStringBuilder-8 1000000000 4.14 ns/op 6 B/op 0 allocs/op
PASS
ok github.com/hechen0/goexp/exps 70.071s
s := fmt.Sprintf("%s%s", []byte(s1), []byte(s2))
[]byte(s1)
转换制作数据副本。将其与发布的其他解决方案进行比较,您能说出您的解决方案的一个优点吗?
strings.Join()
来自“字符串”包
如果你有一个类型不匹配(比如如果你试图加入一个 int 和一个字符串),你做 RANDOMTYPE (你想要改变的东西)
前任:
package main
import (
"fmt"
"strings"
)
var intEX = 0
var stringEX = "hello all you "
var stringEX2 = "people in here"
func main() {
s := []string{stringEX, stringEX2}
fmt.Println(strings.Join(s, ""))
}
输出 :
hello all you people in here
var buffer bytes.Buffer
代替buffer := bytes.NewBufferString("")
。您也不需要任何这些分号:)。