¿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.
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.
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)
}
}
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
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.
- Crecer (int) -> bytes.Buffer # Grow
- Len() int -> bytes.Buffer # Len
- Restablecer() -> bytes.Buffer # Reset
- String () string -> fmt.Stringer
- Write ([] byte) (int, error) -> io.Escritor
- WriteByte(byte) error -> io.ByteWriter
- WriteRune (rune) (int, error) -> bufio.Escritor#WriteRune - bytes.Buffer # WriteRune
- WriteString (string) (int, error) -> io.StringWriter
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í.
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,"")
}
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.
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.
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:
-
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. -
Concat
tiene un rendimiento realmente malo tanto para la velocidad como para el uso de memoria. No lo uses. -
Buffer#Write
yBuffer#WriteString
son básicamente iguales en velocidad, contrariamente a lo que @Dani-Br dijo en el comentario. Considerarstring
es de hecho[]byte
en Go, tiene sentido. - bytes.Buffer básicamente usa la misma solución que
Copy
con contabilidad adicional y otras cosas. -
Copy
yAppend
utilizan un tamaño de arranque de 64, el mismo que los bytes.Buffer -
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:
- Para tareas simples como lo que OP quiere, usaría
Append
oAppendPreAllocate
. Es lo suficientemente rápido y fácil de usar. - Si necesita leer y escribir el búfer al mismo tiempo, use
bytes.Buffer
por supuesto. Para eso está diseñado.
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)
}
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
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
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
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
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
}
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))
}
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
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
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()
}
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.
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))
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)
}
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