Cómo concatenar cadenas de manera eficiente en go

Resuelto Randy Sugianto 'Yuku' asked hace 15 años • 20 respuestas

En Go, a stringes 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:

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

pero eso no parece muy eficiente.

Randy Sugianto 'Yuku' avatar Nov 19 '09 10:11 Randy Sugianto 'Yuku'
Aceptado

Nueva manera:

Desde Go 1.10 hay un strings.Buildertipo; consulte esta respuesta para obtener más detalles .

Vieja forma:

Utilice el bytespaquete. Tiene un Buffertipo 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).

marketer avatar Nov 19 '2009 20:11 marketer

En Go 1.10+ hay 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.


Ejemplo

Es casi lo mismo con bytes.Buffer.

package main

import (
    "strings"
    "fmt"
)

func main() {
    // ZERO-VALUE:
    //
    // It's ready to use from the get-go.
    // You don't need to initialize it.
    var sb strings.Builder

    for i := 0; i < 1000; i++ {
        sb.WriteString("a")
    }

    fmt.Println(sb.String())
}

Haga clic para ver esto en el patio de recreo .


Interfaces compatibles

strings.BuilderLos métodos de se están implementando teniendo en cuenta las interfaces existentes para que pueda cambiar Builderfácilmente al nuevo tipo en su código.

Firma del método Interfaz Descripción
Grow(int) bytes.Buffer Aumenta la capacidad del búfer en la cantidad especificada. Consulte bytes.Buffer#Grow para obtener más información.
Len() int bytes.Buffer Devuelve el número de bytes en el búfer. Consulte bytes.Buffer#Len para obtener más información.
Reset() bytes.Buffer Restablece el búfer para que esté vacío. Consulte bytes.Buffer#Reset para obtener más información.
String() string fmt.Stringer Devuelve el contenido del buffer como una cadena. Consulte fmt.Stringer para obtener más información.
Write([]byte) (int, error) io.Writer Escribe los bytes dados en el búfer. Consulte io.Writer para obtener más información.
WriteByte(byte) error io.ByteWriter Escribe el byte dado en el búfer. Consulte io.ByteWriter para obtener más información.
WriteRune(rune) (int, error) bufio.Writerobytes.Buffer Escribe la runa dada en el búfer. Consulte bufio.Writer#WriteRune o bytes.Buffer#WriteRune para obtener más información.
WriteString(string) (int, error) io.stringWriter Escribe la cadena dada en el búfer. Consulte io.stringWriter para obtener más información.

Diferencias con bytes.Buffer

  • Sólo puede crecer o restablecerse.
  • Tiene un copyCheckmecanismo incorporado que evita copiarlo accidentalmente. En bytes.Buffer, se puede acceder a los bytes subyacentes de esta manera: (*Buffer).Bytes(). strings.Builderpreviene este problema. A veces, sin embargo, esto no es un problema y, en cambio, es lo que se desea. Por ejemplo: para el comportamiento de observación cuando los bytes se pasan a un io.Readeretc.
  • bytes.Buffer.Reset() rebobina y reutiliza el búfer subyacente, mientras que strings.Builder.Reset() no lo hace , separa el búfer.

Nota

  • No copie un strings.Buildervalor ya que almacena en caché los datos subyacentes.
  • Si desea compartir un strings.Buildervalor, utilice un puntero hacia él.

Consulte su código fuente para obtener más detalles, aquí .

Inanc Gumus avatar Dec 13 '2017 16:12 Inanc Gumus

Si conoce la longitud total de la cadena que va a preasignar, entonces la forma más eficaz de concatenar cadenas puede ser utilizar la función incorporada copy. Si no conoce la longitud total de antemano, no utilice copyy lea las otras respuestas.

En mis pruebas, ese enfoque es ~3 veces más rápido que usar bytes.Buffery mucho, mucho más rápido (~12,000x) que usar operator +. Además, utiliza menos memoria.

Creé un caso de prueba para demostrar 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 realizar pruebas:

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)
    }
}
cd1 avatar May 25 '2014 17:05 cd1