In Go ist eine string
ein primitiver Typ. Dies bedeutet, dass er schreibgeschützt ist. Bei jeder Manipulation wird ein neuer String erstellt.
Wenn ich also Strings oft verketten möchte, ohne die Länge des resultierenden Strings zu kennen, wie kann ich das am besten tun?
Der naive Weg wäre:
s := ""
for i := 0; i < 1000; i++ {
s += getShortStringFromSomewhere()
}
return s
aber das scheint nicht sehr effizient zu sein.
Ab Go 1.10 gibt es einen strings.Builder
-Typ, bitte schauen Sie sich diese Antwort für weitere Details an .
Am besten verwenden Sie das Paket bytes
. Es hat einen Buffer
Typ, der io.Writer
implementiert.
package main
import (
"bytes"
"fmt"
)
func main() {
var buffer bytes.Buffer
for i := 0; i < 1000; i++ {
buffer.WriteString("a")
}
fmt.Println(buffer.String())
}
Dies geschieht in der Zeit O(n).
Die effizienteste Methode zum Verketten von Zeichenfolgen ist die Verwendung der integrierten Funktion copy
. In meinen Tests ist dieser Ansatz ~ 3x schneller als mit bytes.Buffer
und viel schneller (~ 12.000x) als mit dem Operator +
. Außerdem wird weniger Speicher benötigt.
Ich habe einen Testfall erstellt, um dies zu beweisen und hier sind die Ergebnisse:
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
Nachfolgend finden Sie den Code zum Testen:
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)
}
}
Ab Go 1.10 gibt es einen strings.Builder
, hier .
Ein Builder wird zum effizienten Erstellen einer Zeichenfolge mithilfe von Write-Methoden verwendet. Das Kopieren des Speichers wird minimiert. Der Nullwert ist gebrauchsfertig.
Verwendung:
Mit bytes.Buffer
Ist es fast genauso.
package main
import (
"strings"
"fmt"
)
func main() {
var str strings.Builder
for i := 0; i < 1000; i++ {
str.WriteString("a")
}
fmt.Println(str.String())
}
Hinweis: Kopieren Sie keinen StringBuilder-Wert, da die zugrunde liegenden Daten zwischengespeichert werden. Wenn Sie einen StringBuilder-Wert freigeben möchten, verwenden Sie Zeiger.
StringBuilder-Methoden und -Schnittstellen, die unterstützt werden:
Die Methoden werden unter Berücksichtigung der vorhandenen Schnittstellen implementiert, sodass Sie in Ihrem Code problemlos auf den neuen Builder wechseln können.
Nullwertverwendung:
var buf strings.Builder
Unterschiede zu Bytes. Puffer:
Es kann nur wachsen oder zurückgesetzt werden.
In bytes.Buffer
Kann auf die zugrunde liegenden Bytes wie folgt zugegriffen werden: (*Buffer).Bytes()
; strings.Builder
Verhindert dieses Problem. Manchmal ist dies jedoch kein Problem und stattdessen erwünscht (z. B. zum Anzeigen von Informationen, wenn die Bytes an einen io.Reader
Usw. übergeben werden).
Außerdem ist ein copyCheck-Mechanismus integriert, der ein versehentliches Kopieren verhindert (func (b *Builder) copyCheck() { ... }
).
Schauen Sie sich den Quellcode an hier.
Im String-Paket gibt es eine Bibliotheksfunktion namens Join
: http://golang.org/pkg/strings/#Join
Ein Blick auf den Code von Join
zeigt einen ähnlichen Ansatz für die Append-Funktion. Kinopiko schrieb: https://golang.org/src/strings/strings.go#L420
Verwendungszweck:
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
Ich habe gerade die oberste Antwort in meinem eigenen Code (ein rekursiver Baumpfad) getestet und der einfache Concat-Operator ist tatsächlich schneller als 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()
}
Dies dauerte 0,81 Sekunden, während der folgende Code:
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
}
dauerte nur 0,61 Sekunden. Dies ist wahrscheinlich auf den Aufwand beim Erstellen der neuen BufferString
zurückzuführen.
Update: Ich habe auch die join
-Funktion getestet und sie lief in 0,54 Sekunden.
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,"")
}
Sie können eine große Anzahl von Bytes erstellen und die Bytes der kurzen Zeichenfolgen mithilfe von Zeichenfolgen in diese hinein kopieren. Es gibt eine Funktion in "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;
}
Wenn die Operationen abgeschlossen sind, verwenden Sie string ( )
für die große Anzahl von Bytes, um sie erneut in eine Zeichenfolge zu konvertieren.
Dies ist die schnellste Lösung, die nicht erforderlich ist. Sie müssen zuerst die Gesamtpuffergröße kennen oder berechnen:
var data []byte
for i := 0; i < 1000; i++ {
data = append(data, getShortStringFromSomewhere()...)
}
return string(data)
Bei meinem benchmark ist es 20% langsamer als die Kopierlösung (8,1 ns pro Anhängen statt 6,72 ns), aber immer noch 55% schneller als bytes.Buffer.
Ab Go 1.10 wird empfohlen, string.Builder
für bytes.Buffer
zu ersetzen. 1.10 Versionshinweise prüfen
Ein neuer Typ-Generator ersetzt Bytes.Buffer für den Anwendungsfall der Ansammlung von Text in einem String-Ergebnis. Die Builder-API ist eine eingeschränkte Teilmenge von Bytes.Buffer, mit der das Kopieren der Daten während der String-Methode sicher vermieden werden kann.
================================================== ==========
Der Benchmark-Code von @ cd1 und andere Antworten sind falsch. b.N
soll nicht in der Benchmark-Funktion gesetzt werden. Es wird vom Go-Test-Tool dynamisch festgelegt, um festzustellen, ob die Ausführungszeit des Tests stabil ist.
Eine Benchmark-Funktion sollte die gleichen Test b.N
-Zeiten ausführen, und der Test innerhalb der Schleife sollte für jede Iteration gleich sein. Also korrigiere ich es durch Hinzufügen einer inneren Schleife. Ich füge auch Benchmarks für einige andere Lösungen hinzu:
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)
}
}
Die Umgebung ist OS X 10.11.6 und 2,2 GHz Intel Core i7
Testergebnisse:
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
Fazit:
CopyPreAllocate
ist der schnellste Weg; AppendPreAllocate
ist ziemlich nahe an Nr. 1, aber es ist einfacher, den Code zu schreiben.Concat
hat eine wirklich schlechte Leistung, sowohl was die Geschwindigkeit als auch den Speicherbedarf angeht. Verwenden Sie es nicht.Buffer#Write
und Buffer#WriteString
grundsätzlich gleich. Betrachtet man string
in der Tat []byte
in Go, ist es sinnvoll.Copy
mit zusätzlicher Buchhaltung und anderem Material.Copy
und Append
verwenden eine Bootstrap-Größe von 64, die der Bytes.Buffer entsprichtAppend
verbrauchen mehr Speicher und Zuweisungen, ich denke, es hängt mit dem verwendeten Wachstumsalgorithmus zusammen. Der Speicher wächst nicht so schnell wie Bytes.BufferVorschlag:
Append
oder AppendPreAllocate
verwenden. Es ist schnell genug und einfach zu bedienen.bytes.Buffer
. Dafür ist es konzipiert.package main
import (
"fmt"
)
func main() {
var str1 = "string1"
var str2 = "string2"
out := fmt.Sprintf("%s %s ",str1, str2)
fmt.Println(out)
}
Mein ursprünglicher Vorschlag war
s12 := fmt.Sprint(s1,s2)
Aber obige Antwort mit bytes.Buffer - WriteString () ist der effizienteste Weg.
Mein ursprünglicher Vorschlag verwendet Reflexion und einen Typwechsel. Siehe (p *pp) doPrint
und (p *pp) printArg
Es gibt keine universelle Stringer () - Schnittstelle für Basistypen, wie ich naiv gedacht hatte.
Zumindest jedoch verwendet Sprint () intern einen bytes.Buffer. Somit
`s12 := fmt.Sprint(s1,s2,s3,s4,...,s1000)`
ist in Bezug auf die Speicherzuordnung akzeptabel.
=> Sprint () - Verkettung kann für eine schnelle Debug-Ausgabe verwendet werden.
=> Andernfalls verwenden Sie bytes.Buffer ... WriteString
Wenn Sie die Antwort von cd1 erweitern: Sie könnten append () anstelle von copy () . Verwenden. Append () macht immer größere Vorbehalte und kostet etwas mehr Speicher, spart jedoch Zeit Ich habe zwei hinzugefügt weitere Benchmarks an der Spitze von Ihnen . Lokal ausführen mit
go test -bench=. -benchtime=100ms
Auf meinem Thinkpad T400s ergibt es:
BenchmarkAppendEmpty 50000000 5.0 ns/op
BenchmarkAppendPrealloc 50000000 3.5 ns/op
BenchmarkCopy 20000000 10.2 ns/op
Dies ist die aktuelle Version von Benchmark, die von @ cd1 (Go 1.8
, linux x86_64
) mit den von @icza und @PickBoy genannten Fehlern bereitgestellt wird.
Bytes.Buffer
ist nur 7
-mal schneller als die direkte Zeichenfolgenverkettung mit dem +
-Operator.
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")
}
}
}
Zeiten:
BenchmarkConcat-4 300000 6869 ns/op
BenchmarkBuffer-4 1000000 1186 ns/op
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()
}
Ich mache es mit folgendem: -
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))
}
Für diejenigen, die aus der Java-Welt kommen, in der wir StringBuilder
für effiziente String-Verkettung haben, scheint die neueste Go-Version ihr Äquivalent zu haben und heißt Builder
: https://github.com/golang/go/blob/master/ src/strings/builder.go