10

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

11

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

Я хочу объединить строки множество раз, не зная длину результирующей строки. Какой самый эффективный способ сделать это?

Наивный способ может выглядеть так:

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

Однако это не кажется очень эффективным решением. Как лучше поступить в данной ситуации?

5 ответ(ов)

1

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

В пакете строк strings есть функция 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{"это", "соединённая", "строка\n"}
    fmt.Printf(strings.Join(s, " "))
}

При выполнении программы вы получите:

это соединённая строка
0

При тестировании кода на производительность, I обнаружил, что использование простого оператора конкатенации строк быстрее, чем 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 секунды, в то время как следующий код, использующий конкатенацию строк, выполнялся быстрее — за 0.61 секунды:

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
} 

Это, вероятно, связано с накладными расходами на создание нового 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,"")
}

Таким образом, если вам важна производительность, стоит рассмотреть использование простого оператора конкатенации или, альтернативно, метода join.

0

Ваш код на Go правильно формирует строку, используя функцию fmt.Sprintf. Давайте я объясню, что здесь происходит:

package main

import (
  "fmt"
)

func main() {
    var str1 = "string1" // Объявляем переменную str1 и инициализируем её строкой "string1"
    var str2 = "string2" // Объявляем переменную str2 и инициализируем её строкой "string2"
    
    // Используем fmt.Sprintf для форматирования строки с использованием переменных str1 и str2
    out := fmt.Sprintf("%s %s ", str1, str2)
    
    // Выводим результат на экран
    fmt.Println(out) // Вывод: "string1 string2 "
}

В этом примере fmt.Sprintf формирует новую строку, вставляя значения str1 и str2 в заданный формат. Шаблон "%s %s " означает, что будут вставлены две строки (по одной для каждой %s), разделенные пробелом. Итоговая строка будет содержать содержимое обеих переменных, а затем выводится на экран.

Если вы хотите убрать лишний пробел в конце, просто измените шаблон на "%s %s" без пробела в конце.

0

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

func Append(slice, data []byte) []byte {
    l := len(slice)
    if l + len(data) > cap(slice) { // перераспределение памяти
        // Выделяем в два раза больше, чем нужно, для будущего роста.
        newSlice := make([]byte, (l+len(data))*2)
        // Копируем данные (можно использовать 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() для преобразования большого среза байт обратно в строку.

0

Это актуальная версия бенчмарка, предоставленная @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

Как видно из результатов, использование bytes.Buffer значительно снижает время выполнения при многократной конкатенации строк, однако разница не так велика, как можно было бы ожидать.

Чтобы ответить на вопрос, пожалуйста, войдите или зарегистрируйтесь