it-swarm.com.de

Wie verkettet man Strings effizient in Go?

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.

586

Hinweis im Jahr 2018 hinzugefügt

Ab Go 1.10 gibt es einen strings.Builder-Typ, bitte schauen Sie sich diese Antwort für weitere Details an .

Pre-201x Antwort

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).

756
marketer

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)
    }
}
252
cd1

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.

158
Inanc Gumus

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
121
mbarkhau

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,"")
}
38
JasonMc

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.

21
user181548

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.

20
rog

Aktualisieren Sie 2018-04-03

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:

  1. CopyPreAllocate ist der schnellste Weg; AppendPreAllocate ist ziemlich nahe an Nr. 1, aber es ist einfacher, den Code zu schreiben.
  2. Concat hat eine wirklich schlechte Leistung, sowohl was die Geschwindigkeit als auch den Speicherbedarf angeht. Verwenden Sie es nicht.
  3. Im Gegensatz zu dem, was @ Dani-Br in dem Kommentar gesagt hat, sind Buffer#Write und Buffer#WriteString grundsätzlich gleich. Betrachtet man string in der Tat []byte in Go, ist es sinnvoll.
  4. bytes.Buffer verwenden grundsätzlich dieselbe Lösung wie Copy mit zusätzlicher Buchhaltung und anderem Material.
  5. Copy und Append verwenden eine Bootstrap-Größe von 64, die der Bytes.Buffer entspricht
  6. Append verbrauchen mehr Speicher und Zuweisungen, ich denke, es hängt mit dem verwendeten Wachstumsalgorithmus zusammen. Der Speicher wächst nicht so schnell wie Bytes.Buffer

Vorschlag:

  1. Für einfache Aufgaben wie das, was OP wünscht, würde ich Append oder AppendPreAllocate verwenden. Es ist schnell genug und einfach zu bedienen.
  2. Wenn Sie den Puffer gleichzeitig lesen und schreiben müssen, verwenden Sie natürlich bytes.Buffer. Dafür ist es konzipiert.
18
PickBoy
package main

import (
  "fmt"
)

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

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

12
Peter Buchmann

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
10
Peter Buchmann

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
2
Vitaly Isaev

goutils.JoinBetween

 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()
}
1
Xian Shu

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
}
1
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))
}
0
rajni kant

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

0
Joel