Pregunta

En Go, un string Es un tipo primitivo, lo que significa que es de solo lectura y cada manipulación 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 forma ingenua sería:

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

pero eso no parece muy eficiente.

¿Fue útil?

Solución

Nota añadida en 2018

En Ir 1.10 hay un tipo strings.Builder, favor tome una mirada a esta respuesta para más detalles .

Respuesta Pre-201x

La mejor forma de hacerlo es utilizar el paquete de href="http://golang.org/pkg/bytes/" rel="noreferrer"> bytes . Tiene una Buffer tipo 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 tiempo O (n).

Otros consejos

La forma más eficiente para concatenar cadenas está utilizando la función incorporada copy . En mis pruebas, este enfoque es ~ 3 veces más rápido que el uso de bytes.Buffer y mucho mucho más rápido (~ 12.000 x) que el uso de la + operador. Además, se usa menos memoria.

He creado un caso de prueba para probar esto y aquí están las 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)
    }
}

A partir de Go 1.10 hay una strings.Builder, aquí.

Un constructor se utiliza 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())
}

Nota: No copie un valor de StringBuilder ya que almacena en caché los datos subyacentes.Si desea compartir un valor de StringBuilder, utilice punteros.


Métodos e interfaces de StringBuilder que admite:

Sus métodos se están implementando teniendo en cuenta las interfaces existentes para que pueda cambiar fácilmente al nuevo Builder en su código.


Uso de valor cero:

var buf strings.Builder

Diferencias con bytes.Buffer:

  • Sólo puede crecer o restablecerse.

  • En bytes.Buffer, se puede acceder a los bytes subyacentes de esta manera: (*Buffer).Bytes(); strings.Builder previene este problema.A veces, esto no es un problema y, en cambio, es lo que se desea (por ejemplo, para observar el comportamiento cuando los bytes se pasan a un io.Reader etc).

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


Mira su código fuente aquí.

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

Un vistazo 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

Yo sólo como punto de referencia la respuesta más común fue anunciado anteriormente en mi propio código (un paseo árbol recursivo) y el simple operador concat es más rápido que el 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()
}

Esto tomó 0.81 segundos, 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
} 

sólo tomó 0.61 segundos. Esto es probablemente debido a la sobrecarga de crear el nuevo BufferString.

Actualización:. también como punto de referencia la función join y corrió en los 0.54 segundos

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

Se puede crear una gran porción de bytes y copiar los bytes de las cadenas cortas en ella usando rebanadas de cuerda. Hay una función dada en "Go eficaz":

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

A continuación, cuando se terminan las operaciones, utilice string ( ) sobre la rebanada grande de bytes para convertirlo en una cadena de nuevo.

Esta es la solución más rápida que no requiere a conocer o calcular el tamaño total de memoria intermedia primero:

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

Por mi referencia , es 20% más lenta que la solución de copia (por 8.1ns en lugar de añadir 6.72ns), pero aún 55% más rápido que usar bytes.Buffer.

package main

import (
  "fmt"
)

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

Nota añadida en 2018

En Ir 1.10 hay un tipo strings.Builder, favor tome una mirada a esta respuesta para más detalles .

Respuesta Pre-201x

El código de prueba de @ CD1 y otras respuestas son incorrectas. No se supone b.N que se fijará en función de referencia. Se fija por la herramienta de prueba de marcha de forma dinámica para determinar si el tiempo de ejecución de la prueba es estable.

Una función de punto de referencia debería ejecutar los mismos tiempos b.N prueba y la prueba dentro del bucle debe ser el mismo para cada iteración. Así lo fijo mediante la adición de un bucle interno. También agrego 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 medio ambiente 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 de No.1, pero es más fácil de escribir el código.
  2. Concat tiene muy mal rendimiento tanto para la velocidad y el uso de memoria. No lo utilice.
  3. Buffer#Write y Buffer#WriteString son básicamente los mismos en la velocidad, al contrario de lo @ Dani-Br dicho en el comentario. Teniendo en cuenta string es de hecho []byte en Go, tiene sentido.
  4. bytes.Buffer utilizan básicamente la misma solución que Copy con libro de mantenimiento extra y otras cosas.
  5. Copy y Append utilizan un tamaño de bootstrap de 64, lo mismo que bytes.Buffer
  6. Append utilizar más memoria y allocs, creo que está relacionado con el algoritmo de cultivo que utilice. No está creciendo la memoria tan rápido como bytes.Buffer

Sugerencia:

  1. Para tarea sencilla, como lo PO quiere, me gustaría utilizar Append o AppendPreAllocate. Es lo suficientemente rápido y fácil de usar.
  2. Si la necesidad de leer y escribir la memoria intermedia al mismo tiempo, utilizar bytes.Buffer por supuesto. Eso es lo que está diseñado para.

Mi sugerencia original

s12 := fmt.Sprint(s1,s2)

Pero por encima de respuesta utilizando bytes.Buffer - WriteString () es el más eficiente camino.

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

Al menos, sin embargo, Sprint () internamente utiliza un bytes.Buffer. Por lo tanto

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

es aceptable en términos de las asignaciones de memoria.

=> Sprint () concatenación se puede utilizar para la salida de depuración rápida.
=> De lo contrario usar bytes.Buffer ... WriteString

Ampliando la respuesta de CD1: Es posible usar append () en lugar de la copia (). append () hace que cada vez más grandes disposiciones anticipadas, con un costo un poco más de memoria, pero el ahorro de tiempo. Añadí dos puntos de referencia más en la parte superior de los suyos. Ejecutar localmente con

go test -bench=. -benchtime=100ms

En mis ThinkPad T400s se produce:

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

Esta es la versión actual del punto de referencia proporcionada por @ CD1 (Go 1.8, linux x86_64) con las correcciones de los errores mencionados por @icza y @PickBoy.

Bytes.Buffer es únicas veces 7 más rápido que la concatenación de cadenas directa 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")
        }
    }
}

Los tiempos:

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

Lo hago utilizando la 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
}
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))
}

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

strings.Builder uso 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
s := fmt.Sprintf("%s%s", []byte(s1), []byte(s2))

strings.Join() del paquete de "cadenas"

Si usted tiene una coincidencia de tipos (como si usted está tratando de unirse a un int y una cadena), hacer RANDOMTYPE (cosa que desea cambiar)

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

Salida:

hello all you people in here
Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top