Jak efektywnie łączyć struny w Go?
W Go, string
jest typem prymitywnym, co oznacza, że jest tylko do odczytu, a każda manipulacja nim utworzy nowy ciąg znaków.
Więc jeśli chcę łączyć ciągi wiele razy, nie znając długości wynikowego ciągu, jaki jest najlepszy sposób, aby to zrobić?
Naiwnym sposobem byłoby:
s := ""
for i := 0; i < 1000; i++ {
s += getShortStringFromSomewhere()
}
return s
Ale to nie wydaje się zbyt efektywne. 20 answers
Najlepszym sposobem jest użycie bytes
paczka. Posiada Buffer
typ, który realizuje 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())
}
To robi to w O (n) czasie.
Uwaga dodana w 2018
Od Go 1.10 są ciągi.Typ Builder , który osiąga to jeszcze efektywniej (dla ciągów). Podany tam przykład jest zwięzły i łatwy do skopiowania/dostosowania.
Jest to analogiczne do klasyStringBuilder w Javie.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-08-01 14:11:27
Najskuteczniejszym sposobem łączenia łańcuchów jest użycie wbudowanej funkcji copy
. W moich testach to podejście jest ~3x szybsze niż przy użyciu bytes.Buffer
i znacznie szybciej (~12 000 x) niż używając operatora +
. Ponadto zużywa mniej pamięci.
Stworzyłem przypadek testowy aby to udowodnić, a oto wyniki:
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
Poniżej znajduje się kod do testów:
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/doraprojects.net/template/agent.layouts/content.php on line 54
2018-06-13 00:33:24
W pakiecie łańcuchów istnieje funkcja biblioteczna o nazwie Join
:
http://golang.org/pkg/strings/#Join
Spojrzenie na kod Join
Pokazuje podobne podejście do dodawania funkcji Kinopiko napisał (a): https://golang.org/src/strings/strings.go#L420
Użycie:
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/doraprojects.net/template/agent.layouts/content.php on line 54
2018-08-28 08:24:36
Począwszy od Go 1.10 jest strings.Builder
, tutaj .
Konstruktor jest używany do efektywnego budowania ciągu znaków za pomocą metod zapisu. Minimalizuje kopiowanie pamięci. Wartość zero jest gotowa do użycia.
Użycie:
To prawie to samo z 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())
}
Metody StringBuilder i interfejsy, które obsługuje:
Jego metody są implementowane z myślą o istniejących interfejsach, dzięki czemu można można łatwo przełączyć się na nowy Konstruktor w kodzie.
- Grow(int) -> bajtów.Bufor # Grow
- Len () int -> bajtów.Bufor # Len
- Reset() -> bajtów.Bufor # Reset
- String () string -> fmt.Stringer
- Write ([] byte) (int, error) -> io.Writer
- WriteByte(byte) error -> io.ByteWriter
- WriteRune(rune) (int, error) -> bufio.Scenarzysta - bajtów.Bufor # WriteRune
- WriteString(string) (int, error) -> io.stringWriter
Użycie wartości zerowej:
var sb strings.Builder
Różnice od bajtów.Bufor:
Jest niezmienny i może tylko rosnąć lub zresetować.
W
bytes.Buffer
bajtach bazowych można escape like this:(*Buffer).Bytes()
;strings.Builder
zapobiega temu problemowi.Posiada również mechanizm copyCheck wewnątrz, który zapobiega przypadkowemu skopiowaniu go (
func (b *Builder) copyCheck() { ... }
).
Sprawdź jego kod źródłowy tutaj.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-06-01 12:22:47
Właśnie porównałem górną odpowiedź zamieszczoną powyżej w moim własnym kodzie (rekurencyjny spacer po drzewie) i prosty operator concat jest w rzeczywistości szybszy niż 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()
}
Trwało to 0,81 s, natomiast następujący kod:
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
}
Zajęło tylko 0,61 s. jest to prawdopodobnie spowodowane kosztami tworzenia nowych buforów.
Update: porównałem również funkcję join i działała w 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/doraprojects.net/template/agent.layouts/content.php on line 54
2012-04-29 02:02:43
Jest to najszybsze rozwiązanie, które nie wymaga najpierw musisz poznać lub obliczyć całkowity rozmiar bufora:
var data []byte
for i := 0; i < 1000; i++ {
data = append(data, getShortStringFromSomewhere()...)
}
return string(data)
Według mojegobenchmark , jest 20% wolniejszy niż rozwiązanie kopiujące (8,1 ns na append zamiast 6.72 ns), ale nadal o 55% szybciej niż przy użyciu bajtów.Bufor.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-09-08 13:13:56
Można utworzyć duży kawałek bajtów i skopiować do niego bajty krótkich łańcuchów za pomocą plasterków. Istnieje funkcja podana w "efektywnym Go":
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;
}
Następnie po zakończeniu operacji użyj string ( )
na dużym kawałku bajtów, aby ponownie przekształcić go w ciąg znaków.
Aktualizacja 2018-04-03
Od teraz 1.10, string.Builder
zaleca się zastąpienie bytes.Buffer
. sprawdź informacje o wydaniu 1.10
Nowy konstruktor typów jest zamiennikiem bajtów.Bufor dla przypadku użycia nagromadzenia tekstu w wyniku ciągu znaków. API Konstruktora jest ograniczonym podzbiorem bajtów.Bufora, który pozwala bezpiecznie uniknąć powielania danych podczas ciągu metoda.
============================================================
Kod odniesienia @cd1 i inne odpowiedzi są błędne. b.N
nie powinien być ustawiony w funkcji benchmark. Jest ustawiany dynamicznie przez narzędzie testowe go, aby określić, czy czas wykonania testu jest stabilny.
Funkcja benchmark powinna wykonywać ten sam test b.N
razy, a test wewnątrz pętli powinien być taki sam dla każdej iteracji. Więc naprawiam to dodając wewnętrzną pętlę. Dodam również benchmarki dla niektórych innych rozwiązań:
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)
}
}
Środowisko to OS X 10.11.6, 2.2 GHz Intel Core i7
Wyniki testu:
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
Wniosek:
-
CopyPreAllocate
jest najszybszym sposobem;AppendPreAllocate
jest dość blisko numeru 1, ale łatwiej jest napisać kod. -
Concat
ma naprawdę złą wydajność zarówno pod względem szybkości, jak i wykorzystania pamięci. Nie używaj go. -
Buffer#Write
iBuffer#WriteString
są w zasadzie takie same w szybkości, wbrew temu, co powiedział @Dani-Br w komentarzu. Rozważającstring
jest rzeczywiście[]byte
W Go, to ma sens. - bajtów.Bufor zasadniczo używa tego samego rozwiązania, co
Copy
z dodatkową księgowością i innymi rzeczami. -
Copy
iAppend
używają rozmiaru bootstrap 64, takiego samego jak bajty.Bufor -
Append
użyj więcej pamięci i allocs, myślę, że jest to związane z algorytmem wzrostu, którego używa. Nie rośnie pamięć tak szybko jak bajty.Bufor
Sugestia:
- do prostych zadań, takich jak to, czego chce OP, użyłbym
Append
lubAppendPreAllocate
. Jest wystarczająco szybki i łatwy w użyciu. - Jeśli chcesz jednocześnie odczytywać i zapisywać bufor, użyj oczywiście
bytes.Buffer
. Do tego jest zaprojektowany.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
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/doraprojects.net/template/agent.layouts/content.php on line 54
2016-03-06 20:55:48
Moja pierwotna sugestia brzmiała
s12 := fmt.Sprint(s1,s2)
Ale powyżej odpowiedz używając bajtów.Bufor-WriteString () jest najbardziej efektywnym sposobem.
Moja początkowa sugestia wykorzystuje odbicie i przełącznik typu. Zobacz (p *pp) doPrint
i (p *pp) printArg
Nie ma uniwersalnego interfejsu Stringer () dla podstawowych typów, jak naiwnie myślałem.
Przynajmniej Sprint () wewnętrznie używa bajtów.Bufor. Tak więc
`s12 := fmt.Sprint(s1,s2,s3,s4,...,s1000)`
Jest akceptowalny pod względem alokacji pamięci.
=> Konkatenacja Sprint() może być używana do szybkiego debugowania.
= > W przeciwnym razie użyj bajtów.Bufor ... WriteString
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-09-11 10:50:11
Rozszerzanie odpowiedzi cd1: Możesz użyć append () zamiast copy (). append() tworzy coraz większe rezerwy z wyprzedzeniem, kosztując trochę więcej pamięci, ale oszczędzając czas. Dodałem jeszcze dwa benchmarki na górze twojego. Uruchom lokalnie z
go test -bench=. -benchtime=100ms
Na moim thinkpad T400s daje:
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/doraprojects.net/template/agent.layouts/content.php on line 54
2015-01-25 02:40:45
To jest aktualna wersja benchmarka dostarczona przez @ cd1 (Go 1.8
, linux x86_64
) z poprawkami błędów wymienionych przez @icza i @PickBoy.
Bytes.Buffer
jest tylko 7
razy szybsza niż bezpośrednia konkatenacja ciągu za pomocą operatora +
.
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")
}
}
}
Timingi:
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/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-02 11:06:51
Dla tych, którzy pochodzą ze świata Javy, gdzie mamy StringBuilder
do efektywnej konkatenacji łańcuchów, wydaje się, że najnowsza wersja go ma swój odpowiednik i nazywa się 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/doraprojects.net/template/agent.layouts/content.php on line 54
2017-11-22 23:35:02
Robię to używając: -
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/doraprojects.net/template/agent.layouts/content.php on line 54
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/doraprojects.net/template/agent.layouts/content.php on line 54
2018-08-09 09:36:39
Wynik testu porównawczego ze statystykami alokacji pamięci. sprawdź kod benchmarka na github .
Użyj sznurków.Kreator optymalizacji wydajności.
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/doraprojects.net/template/agent.layouts/content.php on line 54
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/doraprojects.net/template/agent.layouts/content.php on line 54
2018-09-18 03:01:06
Spójrz na bibliotekę golanga strconv dającą dostęp do kilku funkcji AppendXX, umożliwiając nam łączenie łańcuchów z łańcuchami i innymi typami danych.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
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/doraprojects.net/template/agent.layouts/content.php on line 54
2013-09-04 09:34:09
strings.Join()
z pakietu "strings"
Jeśli masz niedopasowanie typu(np. jeśli próbujesz połączyć int i string), wykonujesz RANDOMTYPE (rzecz, którą chcesz zmienić)
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/doraprojects.net/template/agent.layouts/content.php on line 54
2016-08-29 09:25:58