Как эффективно объединить строки

В Go string является примитивным типом, что означает, что она доступна только для чтения, и каждая манипуляция с ней создаст новую строку.

Так что, если я хочу многократно объединять строки, не зная длины полученной строки, каков наилучший способ сделать это?

Наивным способом будет:

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

но это не кажется очень эффективным.

Ответ 1

Примечание добавлено в 2018 году

Начиная с strings.Builder Go 1.10, есть strings.Builder Тип strings.Builder, пожалуйста, посмотрите на этот ответ для более подробной информации.

Ответ до 201x

Лучший способ - использовать пакет bytes. Он имеет тип Buffer который реализует 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())
}

Это происходит за O (n) раз.

Ответ 2

Наиболее эффективный способ объединения строк - использование встроенной функции copy. В моих тестах этот подход примерно в 3 раза быстрее, чем при использовании bytes.Buffer и намного быстрее (~ 12 000 раз), чем при использовании оператора +. Кроме того, он использует меньше памяти.

Я создал контрольный пример, чтобы доказать это, и вот результаты:

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

Ниже приведен код для тестирования:

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

Ответ 3

Начиная с Go 1.10, есть strings.Builder, здесь.

Построитель используется для эффективного построения строки с использованием методов записи. Это минимизирует копирование памяти. Нулевое значение готово к использованию.


Использование:

Это почти то же самое с 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())
}

Примечание. Не копируйте значение StringBuilder, поскольку оно кэширует базовые данные. Если вы хотите поделиться значением StringBuilder, используйте указатели.


Методы и интерфейсы StringBuilder, которые он поддерживает:

Его методы реализуются с учетом существующих интерфейсов, поэтому вы можете легко переключаться на новый Builder в своем коде.


Использование с нулевым значением:

var buf strings.Builder

Отличия от байтов. Буфер:

  • Он может только расти или сбрасываться.

  • В bytes.Buffer можно получить доступ к нижележащим байтам следующим образом: (*Buffer).Bytes(); strings.Builder предотвращает эту проблему. Иногда это не является проблемой, хотя и желательно вместо этого (например, для просмотра поведения при передаче байтов в io.Reader и т.д.).

  • Он также имеет встроенный механизм copyCheck, который предотвращает его случайное копирование (func (b *Builder) copyCheck() { ... }).


Проверьте его исходный код здесь.

Ответ 4

В пакете строк есть библиотечная функция Join: http://golang.org/pkg/strings/#Join.

Взгляд на код Join показывает похожий подход к функции Append, которую написал Kinopiko: https://golang.org/src/strings/strings.go#L420

Использование:

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

Ответ 5

Я только что проверил верхний ответ, опубликованный выше, в своем собственном коде (рекурсивный BufferString дерева), и простой оператор BufferString самом деле быстрее, чем 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()
}

Это заняло 0,81 секунды, тогда как следующий код:

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
} 

заняло всего 0,61 секунды. Вероятно, это связано с накладными расходами на создание новой BufferString.

Обновление: я также протестировал функцию join и она запустилась за 0,54 секунды.

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

Ответ 6

Вы можете создать большой кусок байтов и скопировать в него байты коротких строк с использованием строковых срезов. В "Эффективном Go" есть функция:

func Append(slice, data[]byte) []byte {
    l := len(slice);
    if l + len(data) > cap(slice) { // reallocate
        // Allocate double what 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;
}

Затем, когда операции закончены, используйте string ( ) в большом куске байтов, чтобы снова преобразовать его в строку.

Ответ 7

Это самое быстрое решение, которое не требует вы сначала должны знать или рассчитать общий размер буфера:

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

По моему тегу он на 20% медленнее, чем решение для копирования (8.1ns per append, а не 6.72ns), но все же на 55% быстрее, чем использование байтов .Buffer.

Ответ 8

package main

import (
  "fmt"
)

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

Ответ 9

Примечание добавлено в 2018 году

Начиная с strings.Builder Go 1.10, есть strings.Builder Тип strings.Builder, пожалуйста, посмотрите на этот ответ для более подробной информации.

Ответ до 201x

Код теста @cd1 и другие ответы неверны. bN не должен быть установлен в контрольной функции. Он устанавливается инструментом динамического тестирования, чтобы определить, стабильно ли время выполнения теста.

Функция эталонного теста должна запускать один и тот же тест bN раз, и тест внутри цикла должен быть одинаковым для каждой итерации. Поэтому я исправляю это, добавляя внутренний цикл. Я также добавлю тесты для некоторых других решений:

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

Окружающая среда - OS X 10.11.6, 2,2 ГГц Intel Core i7

Результаты теста:

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

Заключение:

  1. CopyPreAllocate - самый быстрый способ; AppendPreAllocate довольно близок к № 1, но легче написать код.
  2. Concat очень Concat производительность как по скорости, так и по использованию памяти. Не используйте это.
  3. Buffer#Write и Buffer#WriteString в основном одинаковы по скорости, в отличие от того, что @Dani-Br сказал в комментарии. Учитывая, что string действительно []byte в Go, это имеет смысл.
  4. bytes.Buffer в основном использует то же решение, что и Copy с дополнительным ведением бухгалтерского учета и другими вещами.
  5. Copy и Append используют размер начальной загрузки 64, такой же, как в байтах. Буфер
  6. Append использовать больше памяти и выделяет, я думаю, это связано с алгоритмом роста, который он использует. Это не растущая память так быстро, как байты.

Предложение:

  1. Для простых задач, таких как то, что хочет OP, я бы использовал Append или AppendPreAllocate. Это достаточно быстро и просто в использовании.
  2. Если необходимо одновременно прочитать и записать буфер, используйте bytes.Buffer. Вот для чего он предназначен.

Ответ 10

Мое первоначальное предложение было

s12 := fmt.Sprint(s1,s2)

Но выше ответ, используя bytes.Buffer - WriteString() является наиболее эффективным способом.

В моем первоначальном предложении используется отражение и переключатель типа. См. (p *pp) doPrint и (p *pp) printArg
Для базовых типов нет универсального интерфейса Stringer(), как я наивно думал.

По крайней мере, хотя Sprint() внутренне использует bytes.Buffer. Таким образом,

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

является приемлемым с точки зрения распределения памяти.

= > Конкатенация Sprint() может использоваться для быстрого вывода отладки.
= > В противном случае используйте bytes.Buffer... WriteString

Ответ 11

Расширение ответа cd1: Вы можете использовать append() вместо copy(). append() делает все более значительные предварительные положения, обходится немного больше памяти, но экономит время. Я добавил еще два теста в верхней части экрана. Запустите локально с помощью

go test -bench=. -benchtime=100ms

На моем Thinkpad T400s он дает:

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

Ответ 12

Это фактическая версия теста, предоставленная @cd1 (Go 1.8, linux x86_64) с исправлениями ошибок, упомянутых @icza и @PickBoy.

Bytes.Buffer всего 7 раз быстрее, чем прямая конкатенация строк с помощью оператора +.

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

Тайминги:

BenchmarkConcat-4                             300000          6869 ns/op
BenchmarkBuffer-4                            1000000          1186 ns/op

Ответ 13

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

Ответ 14

Я делаю это, используя следующее: -

package main

import (
    "fmt"
    "strings"
)

func main (){
    concatenation:= strings.Join([]string{"a","b","c"},"") //where second parameter is a separator. 
    fmt.Println(concatenation) //abc
}

Ответ 15

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

Ответ 16

Для тех, кто пришел из мира Java, где у нас StringBuilder для эффективной конкатенации строк, похоже, что последняя версия go имеет свой эквивалент и называется Builder: https://github.com/golang/go/blob/master/src/strings/builder.go

Ответ 17

Взгляните на библиотеку golang strconv, предоставляющую доступ к нескольким функциям AppendXX, что позволяет нам объединять строки со строками и другими типами данных.

Ответ 18

Результат теста со статистикой выделения памяти. проверьте код теста на github.

использовать строки. Builder для оптимизации производительности.

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

Ответ 19

s := fmt.Sprintf("%s%s", []byte(s1), []byte(s2))

Ответ 20

strings.Join() из пакета "strings"

Если у вас есть несоответствие типов (например, если вы пытаетесь соединить int и строку), вы делаете RANDOMTYPE (вещь, которую вы хотите изменить)

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

Выход :

hello all you people in here