Cómo concatenar cadenas de manera eficiente en go
En Go, a 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:
var s string
for i := 0; i < 1000; i++ {
s += getShortStringFromSomewhere()
}
return s
pero eso no parece muy eficiente.
Nueva manera:
Desde Go 1.10 hay un strings.Builder
tipo; consulte esta respuesta para obtener más detalles .
Vieja forma:
Utilice el bytes
paquete. Tiene un 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).
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.Builder
Los métodos de se están implementando teniendo en cuenta las interfaces existentes para que pueda cambiar Builder
fá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.Writer obytes.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
copyCheck
mecanismo incorporado que evita copiarlo accidentalmente. Enbytes.Buffer
, se puede acceder a los bytes subyacentes de esta manera:(*Buffer).Bytes()
.strings.Builder
previene 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 unio.Reader
etc. bytes.Buffer.Reset()
rebobina y reutiliza el búfer subyacente, mientras questrings.Builder.Reset()
no lo hace , separa el búfer.
Nota
- No copie un
strings.Builder
valor ya que almacena en caché los datos subyacentes. - Si desea compartir un
strings.Builder
valor, utilice un puntero hacia él.
Consulte su código fuente para obtener más detalles, aquí .
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 copy
y lea las otras respuestas.
En mis pruebas, ese enfoque es ~3 veces más rápido que usar bytes.Buffer
y 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)
}
}