¿Cómo concatenar cadenas de manera eficiente en Go?


En Go, un string es un tipo primitivo, lo que significa que es de solo lectura, y cada manipulación de él creará una nueva cadena.

Entonces, si quiero concatenar cadenas muchas veces sin saber la longitud de la cadena resultante, ¿cuál es la mejor manera de hacerlo?

La manera ingenua sería:

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

Pero eso no parece muy eficiente.

Author: jlucktay, 2009-11-19

20 answers

La mejor manera es utilizar el bytes paquete. Tiene un Buffer clase que implementa 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())
}

Esto lo hace en O(n) tiempo.

Nota añadida en 2018

Desde Go 1.10 están las cadenas .Builder tipo, que logra esto aún más eficientemente (para cadenas). El ejemplo dado es sucinto y fácil de copiar / adaptar.

Esto es análogo a la clase StringBuilder en Java.

 722
Author: marketer,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-08-01 14:11:27

La forma más eficiente de concatenar cadenas es usando la función builtin copy. En mis pruebas, ese enfoque es ~3 veces más rápido que usar bytes.Buffer y mucho más rápido (~12,000 x) que usar el operador +. Además, usa menos memoria.

He creado un caso de prueba para probar esto, y aquí están los resultados:

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

A continuación se muestra el código para la prueba:

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)
    }
}
 245
Author: cd1,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-06-13 00:33:24

Hay una función de biblioteca en el paquete strings llamada Join: http://golang.org/pkg/strings/#Join

Una mirada al código de Join muestra un enfoque similar para Anexar la función Kinopiko escribió: https://golang.org/src/strings/strings.go#L420

Uso:

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
 119
Author: mbarkhau,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-08-28 08:24:36

Comenzando con Ir 1.10 hay un strings.Builder, aquí.

Un Constructor se usa para construir eficientemente una cadena usando métodos de escritura. Minimiza la copia de memoria. El valor cero está listo para usar.


Uso:

Es casi lo mismo con 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())
}

Métodos e interfaces StringBuilder que soporta:

Sus métodos se están implementando con las interfaces existentes en mente para que puede cambiar al nuevo Constructor fácilmente en su código.


Uso de valor cero:

var sb strings.Builder

Diferencias de bytes.Buffer:

  • Es inmutable y solo puede crecer o restablecerse.

  • En bytes.Buffer los bytes subyacentes pueden escapar así: (*Buffer).Bytes(); strings.Builder previene este problema.

  • También tiene un mecanismo de copyCheck en su interior que evita copiarlo accidentalmente (func (b *Builder) copyCheck() { ... }).


Echa un vistazo a su código fuente aquí.

 60
Author: Inanc Gumus,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-06-01 12:22:47

Acabo de comparar la respuesta superior publicada anteriormente en mi propio código (una caminata recursiva del árbol) y el operador concat simple es en realidad más rápido que la cadena de búfer.

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

Esto tomó 0.81 s, mientras que el siguiente código:

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
} 

Solo tomó 0.61 s. Esto es probablemente debido a la sobrecarga de la creación de los nuevos BufferStrings.

Actualización: También hice referencia a la función join y se ejecutó en 0.54 s

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,"")
}
 37
Author: JasonMc,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-04-29 02:02:43

Esta es la solución más rápida que no requiere primero debe saber o calcular el tamaño total del búfer:

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

Por mi punto de referencia , es 20% más lento que la solución de copia (8.1 ns por append en lugar de 6.72 ns), pero aún así un 55% más rápido que el uso de bytes.Búfer.

 19
Author: rog,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-09-08 13:13:56

Puede crear un gran segmento de bytes y copiar los bytes de las cadenas cortas en él utilizando segmentos de cadena. Hay una función dada en "Go efectivo":

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

Luego, cuando las operaciones hayan terminado, use string ( ) en la gran porción de bytes para convertirla en una cadena de nuevo.

 18
Author: ,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-11-19 03:57:38

Actualización 2018-04-03

A partir de Go 1.10, string.Builder se recomienda sustituir a bytes.Buffer. Comprobar 1.10 notas de la versión

Un nuevo generador de tipos es un reemplazo para bytes.Buffer para el caso de uso de acumular texto en un resultado de cadena. La API del constructor es un subconjunto restringido de bytes.Buffer que le permite evitar de forma segura hacer una copia duplicada de los datos durante la cadena método.

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

El código de referencia de @cd1 y otras respuestas son incorrectas. b.N no se supone que se establezca en la función de referencia. La herramienta de prueba go lo establece dinámicamente para determinar si el tiempo de ejecución de la prueba es estable.

Una función de referencia debe ejecutar la misma prueba b.N veces y la prueba dentro del bucle debe ser la misma para cada iteración. Así que lo arreglo añadiendo un bucle interno. También añado puntos de referencia para algunas otras soluciones:

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

El entorno es OS X 10.11.6, 2.2 GHz Intel Core i7

Resultados de la prueba:

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

Conclusión:

  1. CopyPreAllocate es la forma más rápida; AppendPreAllocate está bastante cerca del número 1, pero es más fácil escribir el código.
  2. Concat tiene un rendimiento realmente malo tanto para la velocidad como para el uso de memoria. No lo uses.
  3. Buffer#Write y Buffer#WriteString son básicamente iguales en velocidad, contrariamente a lo que @Dani-Br dijo en el comentario. Considerar string es de hecho []byte en Go, tiene sentido.
  4. bytes.Buffer básicamente usa la misma solución que Copy con contabilidad adicional y otras cosas.
  5. Copy y Append utilizan un tamaño de arranque de 64, el mismo que los bytes.Buffer
  6. Append utilizar más memoria y asignaciones, creo que está relacionado con el crecimiento algoritmo que utilice. No está creciendo la memoria tan rápido como los bytes.Buffer

Sugerencia:

  1. Para tareas simples como lo que OP quiere, usaría Append o AppendPreAllocate. Es lo suficientemente rápido y fácil de usar.
  2. Si necesita leer y escribir el búfer al mismo tiempo, use bytes.Buffer por supuesto. Para eso está diseñado.
 17
Author: PickBoy,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-04-03 17:26:12
package main

import (
  "fmt"
)

func main() {
    var str1 = "string1"
    var str2 = "string2"
    out := fmt.Sprintf("%s %s ",str1, str2)
    fmt.Println(out)
}
 15
Author: harold ramos,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-03-06 20:55:48

Mi sugerencia original fue

s12 := fmt.Sprint(s1,s2)

Pero arriba responde usando bytes.Buffer-WriteString () es la forma más eficiente.

Mi sugerencia inicial utiliza la reflexión y un interruptor de tipo. Véase (p *pp) doPrint y (p *pp) printArg
No hay una interfaz universal Stringer() para los tipos básicos, como ingenuamente había pensado.

Al menos, Sprint() internamente usa un byte.Búfer. Así

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

Es aceptable en términos de asignaciones de memoria.

=> La concatenación Sprint () se puede utilizar para la salida de depuración rápida.
= > De lo contrario use bytes.Búfer ... WriteString

 12
Author: Peter Buchmann,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-09-11 10:50:11

Ampliando la respuesta de cd1: Puedes usar append () en lugar de copy (). append () hace provisiones anticipadas cada vez mayores, costando un poco más de memoria, pero ahorrando tiempo. He añadido dos puntos de referencia más en la parte superior de la suya. Ejecutar localmente con

go test -bench=. -benchtime=100ms

En mi thinkpad T400s produce:

BenchmarkAppendEmpty    50000000         5.0 ns/op
BenchmarkAppendPrealloc 50000000         3.5 ns/op
BenchmarkCopy           20000000        10.2 ns/op
 10
Author: Peter Buchmann,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-01-25 02:40:45

Esta es la versión real de benchmark proporcionada por @cd1 (Go 1.8, linux x86_64) con las correcciones de errores mencionados por @icza y @ PickBoy.

Bytes.Buffer es solo 7 veces más rápido que la concatenación directa de cadenas a través del operador +.

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

Tiempos:

BenchmarkConcat-4                             300000          6869 ns/op
BenchmarkBuffer-4                            1000000          1186 ns/op
 1
Author: Vitaly Isaev,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-02 11:06:51

Para aquellos que vienen del mundo Java donde tenemos StringBuilder para una concatenación de cadenas eficiente, parece que la última versión de go tiene su equivalente y se llama Builder: https://github.com/golang/go/blob/master/src/strings/builder.go

 0
Author: Joel,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-11-22 23:35:02

Lo hago usando lo siguiente: -

package main

import (
    "fmt"
    "strings"
)

func main (){
    concatenation:= strings.Join([]string{"a","b","c"},"") //where second parameter is a separator. 
    fmt.Println(concatenation) //abc
}
 0
Author: Krish Bhanushali,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-01-26 00:52:48
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
Author: rajni kant,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-08-09 09:36:39

Resultado de benchmark con estadísticas de asignación de memoria. compruebe el código de referencia en github .

Usa cadenas.Constructor para optimizar el rendimiento.

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
 0
Author: hechen0,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-09-05 16:30:42

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()
}
 0
Author: Xian Shu,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-09-18 03:01:06

Eche un vistazo a la biblioteca de golang strconv que da acceso a varias funciones AppendXX, lo que nos permite concatenar cadenas con cadenas y otros tipos de datos.

 -1
Author: Akash Mishra,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-06-28 19:17:03
s := fmt.Sprintf("%s%s", []byte(s1), []byte(s2))
 -2
Author: user2288856,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-09-04 09:34:09

strings.Join() del paquete "strings"

Si tienes un error de tipo (como si estás intentando unir un int y una cadena), haces RANDOMTYPE (cosa que quieres cambiar)

EX:

package main

import "strings"

var intEX = 0
var stringEX = "hello all you "
var stringEX2 = " people in here"

func main() {
    strings.Join(stringEX, string(intEX), stringEX2)
}
 -2
Author: liam,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-08-29 09:25:58