Question

Go, un string est un type primitif, ce qui signifie qu'il est en lecture seule, et chaque manipulation de celui-ci va créer une nouvelle chaîne.

Donc, si je veux concaténer des chaînes à plusieurs reprises sans connaître la longueur de la chaîne résultante, quelle est la meilleure façon de le faire?

La façon naïve serait:

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

mais cela ne semble pas très efficace.

Était-ce utile?

La solution

Note ajoutée en 2018

A partir de 1.10 Go il y a un type de strings.Builder, s'il vous plaît prendre un coup d'oeil à cette réponse pour plus de détails .

Réponse pré-201x

La meilleure façon est d'utiliser le package bytes. Il a un type Buffer qui implémente 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())
}

Ce qu'il fait en O (n).

Autres conseils

La façon la plus efficace de concaténer des chaînes utilise la fonction copy . Dans mes tests, cette approche est ~ 3 fois plus rapide que d'utiliser bytes.Buffer et beaucoup plus rapide (~ 12 000 x) à l'aide de la + de l'opérateur. En outre, il utilise moins de mémoire.

J'ai créé un test pour prouver et voici les résultats:

 
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

est le code ci-dessous pour le test:

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

À partir de 1.10 Go il y a un strings.Builder , .

  

Un Builder est utilisé pour construire efficacement une chaîne en utilisant des méthodes d'écriture. Il minimise la copie de la mémoire. La valeur zéro est prêt à l'emploi.


Utilisation:

Il est presque la même chose avec 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())
}

Remarque: Ne pas copier une valeur StringBuilder comme il met en cache les données sous-jacentes. Si vous souhaitez partager une valeur StringBuilder, utiliser des pointeurs.


méthodes de StringBuilder et des interfaces, il prend en charge:

Ses méthodes sont mises en œuvre avec les interfaces existantes à l'esprit de sorte que vous pouvez passer à la nouvelle Builder facilement dans votre code.


Utilisation de la valeur zéro:

var buf strings.Builder

Les différences de bytes.Buffer:

  • Il ne peut se développer ou réinitialiser.

  • Dans bytes.Buffer, on peut accéder aux octets sous-jacents comme celui-ci: (*Buffer).Bytes(); strings.Builder empêche ce problème. Parfois, ce n'est pas un problème si et souhaité à la place (par exemple, pour jeter un oeil comportement lorsque les octets sont transmis à un io.Reader etc).

  • Il a également un mécanisme intégré de copyCheck qui empêche la copie accidentially il (func (b *Builder) copyCheck() { ... }).


Consultez son code source ici .

Il y a une fonction de bibliothèque dans le paquet de cordes appelé Join: http://golang.org/pkg/strings/#Join

Un regard sur le code de Join montre une approche similaire à la fonction Append Kinopiko a écrit: https : //golang.org/src/strings/strings.go#L420

Utilisation:

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

Je viens benchmarkée la réponse ci-dessus dans le dessus affiché mon propre code (un parcours d'arbre récursif) et le simple opérateur concat est plus rapide que le 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()
}

a pris 0,81 secondes, alors que le code suivant:

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
} 

a seulement pris 0,61 secondes. Ceci est probablement dû à la surcharge de la création du nouveau BufferString.

Mise à jour:. J'ai aussi benchmarkée la fonction join et il a couru en 0,54 secondes

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

Vous pouvez créer une grande tranche d'octets et copier les octets des chaînes courtes en tranches à l'aide de cordes. Il y a une fonction donnée dans « efficace 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;
}

Ensuite, lorsque les opérations sont terminées, utilisez string ( ) sur la grande tranche d'octets pour le convertir en une chaîne à nouveau.

Ceci est la solution la plus rapide qui ne nécessite pas vous savez ou calculer la taille globale du premier tampon:

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

Par mon , il est 20% plus lent que la solution de copie (8.1ns par ajouter plutôt que 6.72ns), mais encore 55% plus rapide que l'utilisation bytes.Buffer.

package main

import (
  "fmt"
)

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

Note ajoutée en 2018

A partir de 1.10 Go il y a un type de strings.Builder, s'il vous plaît prendre un coup d'oeil à cette réponse pour plus de détails .

Réponse pré-201x

Le code de référence de @ CD1 et d'autres réponses sont fausses. b.N n'est pas censé être mis en fonction de référence. Il est fixé par l'outil de test de passer de façon dynamique afin de déterminer si le temps d'exécution du test est stable.

Une fonction de référence doit exécuter le même test de temps b.N et le test à l'intérieur de la boucle doit être la même pour chaque itération. Donc je résoudre ce problème en ajoutant une boucle intérieure. Je voudrais également ajouter des repères pour d'autres solutions:

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

L'environnement est OS X 10.11.6, 2,2 GHz Intel Core i7

Les résultats du test:

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

Conclusion:

  1. CopyPreAllocate est le moyen le plus rapide; AppendPreAllocate est assez proche de n ° 1, mais il est plus facile d'écrire le code.
  2. Concat a vraiment mauvaise performance à la fois pour la vitesse et l'utilisation de la mémoire. Ne l'utilisez pas.
  3. Buffer#Write et Buffer#WriteString sont fondamentalement les mêmes vitesse, contrairement à ce que @ Dani-Br dit dans le commentaire. Considérant string est en effet []byte dans Go, il est logique.
  4. bytes.Buffer utiliser essentiellement la même solution que Copy avec la tenue de livres supplémentaires et d'autres choses.
  5. Copy et Append utilisent une taille bootstrap de 64, le même que bytes.Buffer
  6. Append utiliser plus de mémoire et allocs, je pense qu'il est lié à l'algorithme de culture qu'il utilise. Il ne grandit pas aussi vite que la mémoire bytes.Buffer

Suggestion:

  1. Pour la tâche simple, comme ce que l'OP veut, j'utiliser Append ou AppendPreAllocate. Il est assez rapide et facile à utiliser.
  2. Si besoin de lire et d'écrire le tampon en même temps, utilisez bytes.Buffer bien sûr. C'est ce qu'il est conçu pour.

Ma suggestion originale était

s12 := fmt.Sprint(s1,s2)

Mais réponse ci-dessus en utilisant bytes.Buffer - WriteString () est le plus efficace manière.

Ma suggestion initiale utilise la réflexion et un commutateur de type. Voir (p *pp) doPrint et (p *pp) printArg
Il n'y a pas d'interface Stringer () universelle pour les types de base, comme je l'avais pensé naïvement.

Au moins cependant, Sprint () interne utilise un bytes.Buffer. Ainsi,

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

est acceptable en termes d'allocations de mémoire.

=> Sprint () concaténation peut être utilisé pour la sortie de débogage rapide.
=> Sinon, utilisez bytes.Buffer ... WriteString

L'expansion sur la réponse CD1: Vous pouvez utiliser append () au lieu de copier (). append () rend toujours plus grandes dispositions à l'avance, ce qui coûte un peu plus de mémoire, mais un gain de temps. J'ai ajouté deux autres points de repère en haut de la vôtre. Exécuter localement avec

go test -bench=. -benchtime=100ms

Sur mes T400s il donne thinkpad:

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

est une version réelle de référence fournie par @ CD1 (Go 1.8, linux x86_64) avec les corrections de bugs mentionnés par @icza et @PickBoy.

Bytes.Buffer est seulement temps de 7 plus rapide que la concaténation de chaîne directe par l'opérateur de +.

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")
        }
    }
}

synchronisations:

BenchmarkConcat-4                             300000          6869 ns/op
BenchmarkBuffer-4                            1000000          1186 ns/op

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()
}

Je le fais en utilisant les éléments suivants: -

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

Résultat de référence avec les statistiques d'allocation de mémoire. vérifier le code de référence à github .

utilisation strings.Builder pour optimiser les performances.

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

strings.Join() du paquet "strings"

Si vous avez une incompatibilité de type (comme si vous essayez de joindre un entier et une chaîne), vous ne RANDOMTYPE (chose que vous voulez changer)

EX:

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

Sortie:

hello all you people in here
Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top