├── .gitignore ├── LICENSE ├── README.md ├── go.mod ├── string.go ├── string_test.go └── stringbuf_bench_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | *.exe 2 | *.exe~ 3 | *.dll 4 | *.so 5 | *.dylib 6 | *.test 7 | *.out 8 | 9 | 10 | /build/ 11 | /bin/ 12 | /dist/ 13 | 14 | /vendor/ 15 | 16 | .vscode/ 17 | .idea/ 18 | *.swp 19 | *.swo 20 | *.DS_Store 21 | 22 | *.cover 23 | *.prof 24 | *.log 25 | 26 | *.tmp 27 | *.bak 28 | *.old 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 stanNthe5 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # stringbuf 2 | 3 | A Go string concatenation library that is more efficient than strings.Builder. 4 | 5 | ## Install 6 | ``` 7 | go get github.com/stanNthe5/stringbuf 8 | ``` 9 | 10 | ## Usage 11 | ``` 12 | sb := stringbuf.New("Hello ", "world,") 13 | sb.Append("I am ", "StringBuf") 14 | sb.Prepend("StringbBuf ", "testing: ") 15 | str := sb.String() 16 | ``` 17 | 18 | ## Benchmark 19 | 20 | ### Compare with strings.Builder 21 | ``` 22 | // stringbuf_bench_test.go 23 | go test -bench=. -benchmem 24 | ``` 25 | 26 | ``` 27 | cpu: Intel(R) Core(TM) i5-8400 CPU @ 2.80GHz 28 | BenchmarkStringBuf_Append-6 10000 209105 ns/op 479674 B/op 16 allocs/op 29 | BenchmarkStringsBuilder_Append-6 1087 1063544 ns/op 1979769 B/op 24 allocs/op 30 | BenchmarkStringBuf_Prepend-6 10000 207060 ns/op 479674 B/op 16 allocs/op 31 | BenchmarkStringsBuilder_PrependSimulated-6 14 81451154 ns/op 407881812 B/op 2014 allocs/op 32 | ``` 33 | 34 | ## Why is stringbuf faster? 35 | 36 | It defers the actual string concatenation (copying data) until `String()` or `Bytes()` is called. During Append or Prepend, it primarily stores references to the input strings in internal `[][]string` slices, chunking them to reduce reallocations compared to strings.Builder which might repeatedly reallocate and copy the growing byte buffer during every append. Prepending is also handled efficiently using a separate buffer, avoiding costly data shifting. (Inspired by the Node.js v8 engine) 37 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/stanNthe5/stringbuf 2 | 3 | go 1.24.0 4 | -------------------------------------------------------------------------------- /string.go: -------------------------------------------------------------------------------- 1 | package stringbuf 2 | 3 | import ( 4 | "bytes" 5 | "unsafe" 6 | ) 7 | 8 | type StringBuf struct { 9 | buf [][]string // for Append() 10 | reverseBuf [][]string // for Prepend() 11 | index int 12 | reverseIndex int 13 | len int 14 | } 15 | 16 | func (s *StringBuf) Write(p []byte) (n int, err error) { 17 | s.Append(string(p)) 18 | return len(p), nil 19 | } 20 | 21 | func (s *StringBuf) WriteString(str string) (int, error) { 22 | if len(str) == 0 { 23 | return 0, nil 24 | } 25 | if len(s.buf) == 0 { 26 | s.buf = append(s.buf, []string{}) 27 | } 28 | if len(s.buf[s.index]) > 1023 { 29 | s.index++ 30 | if len(s.buf) < s.index+1 { 31 | s.buf = append(s.buf, make([]string, 0, 1024)) 32 | } 33 | } 34 | s.buf[s.index] = append(s.buf[s.index], str) 35 | s.len += len(str) 36 | return len(str), nil 37 | } 38 | 39 | func (s *StringBuf) prependStr(str string) { 40 | if len(str) == 0 { 41 | return 42 | } 43 | if len(s.reverseBuf) == 0 { 44 | s.reverseBuf = append(s.reverseBuf, []string{}) 45 | } 46 | if len(s.reverseBuf[s.reverseIndex]) > 1023 { 47 | s.reverseIndex++ 48 | if len(s.reverseBuf) < s.reverseIndex+1 { 49 | s.reverseBuf = append(s.reverseBuf, make([]string, 0, 1024)) 50 | } 51 | } 52 | s.len += len(str) 53 | s.reverseBuf[s.reverseIndex] = append(s.reverseBuf[s.reverseIndex], str) 54 | } 55 | 56 | func (s *StringBuf) Append(strs ...string) { 57 | if len(strs) == 0 { 58 | return 59 | } 60 | for _, str := range strs { 61 | s.WriteString(str) 62 | } 63 | } 64 | 65 | func (s *StringBuf) AppendRune(runes ...rune) { 66 | if len(runes) == 0 { 67 | return 68 | } 69 | for _, r := range runes { 70 | s.WriteString(string(r)) 71 | } 72 | } 73 | 74 | func (s *StringBuf) AppendByte(bytesArr ...[]byte) { 75 | if len(bytesArr) == 0 { 76 | return 77 | } 78 | for _, r := range bytesArr { 79 | s.WriteString(string(r)) 80 | } 81 | } 82 | 83 | func (s *StringBuf) Prepend(strs ...string) { 84 | if len(s.reverseBuf) == 0 { 85 | s.reverseBuf = append(s.reverseBuf, []string{}) 86 | } 87 | 88 | for i := len(strs) - 1; i >= 0; i-- { 89 | if len(strs[i]) == 0 { 90 | continue 91 | } 92 | if len(s.reverseBuf[s.reverseIndex]) > 1023 { 93 | s.reverseIndex++ 94 | if len(s.reverseBuf) < s.reverseIndex+1 { 95 | s.reverseBuf = append(s.reverseBuf, make([]string, 0, 1024)) 96 | } 97 | } 98 | s.len += len(strs[i]) 99 | s.reverseBuf[s.reverseIndex] = append(s.reverseBuf[s.reverseIndex], strs[i]) 100 | } 101 | } 102 | 103 | func (s *StringBuf) PrependRune(runes ...rune) { 104 | if len(runes) == 0 { 105 | return 106 | } 107 | for i := len(runes) - 1; i >= 0; i-- { 108 | s.prependStr(string(runes[i])) 109 | } 110 | } 111 | 112 | func (s *StringBuf) PrependByte(bytesArr ...[]byte) { 113 | if len(bytesArr) == 0 { 114 | return 115 | } 116 | for i := len(bytesArr) - 1; i >= 0; i-- { 117 | s.prependStr(string(bytesArr[i])) 118 | } 119 | } 120 | 121 | func (s *StringBuf) String() string { 122 | if s.len == 0 { 123 | return "" 124 | } 125 | // safe: Bytes() returns freshly allocated, immutable data 126 | return unsafe.String(unsafe.SliceData(s.Bytes()), s.len) 127 | } 128 | 129 | func (s *StringBuf) Bytes() []byte { 130 | var b = make([]byte, 0, s.len) 131 | 132 | for i := len(s.reverseBuf) - 1; i >= 0; i-- { 133 | for j := len(s.reverseBuf[i]) - 1; j >= 0; j-- { 134 | b = append(b, s.reverseBuf[i][j]...) 135 | } 136 | } 137 | 138 | for _, chunk := range s.buf { 139 | for _, str := range chunk { 140 | b = append(b, str...) 141 | } 142 | } 143 | return b 144 | } 145 | 146 | func (s *StringBuf) Equal(t StringBuf) bool { 147 | if s.len != t.len { 148 | return false 149 | } 150 | return bytes.Equal(s.Bytes(), t.Bytes()) 151 | } 152 | 153 | func (s *StringBuf) Reset() { 154 | if s.len == 0 { 155 | return 156 | } 157 | s.buf = s.buf[:0] 158 | s.reverseBuf = s.reverseBuf[:0] 159 | s.len = 0 160 | s.index = 0 161 | s.reverseIndex = 0 162 | } 163 | 164 | func (s *StringBuf) Len() int { 165 | return s.len 166 | } 167 | 168 | func New[T string | []byte](inputs ...T) StringBuf { 169 | var sb StringBuf 170 | for _, input := range inputs { 171 | switch input := any(input).(type) { 172 | case string: 173 | sb.Append(input) 174 | case []byte: 175 | sb.AppendByte(input) 176 | } 177 | } 178 | return sb 179 | } 180 | -------------------------------------------------------------------------------- /string_test.go: -------------------------------------------------------------------------------- 1 | package stringbuf 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestString(t *testing.T) { 8 | sb := New("Hello ", "world") 9 | sb.AppendRune('!') 10 | sb.Append(" I am ", "StringBuf") 11 | sb.Prepend("StringbBuf ", "testing: ") 12 | str := sb.String() 13 | expectedStr := "StringbBuf testing: Hello world! I am StringBuf" 14 | if sb.String() != expectedStr { 15 | t.Errorf("Expected \n\"%s\" \n but got \n\"%s\"", expectedStr, str) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /stringbuf_bench_test.go: -------------------------------------------------------------------------------- 1 | package stringbuf 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | ) 7 | 8 | const sample = "abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghij" 9 | const times = 2000 10 | 11 | // stringbuf 12 | func BenchmarkStringBuf_Append(b *testing.B) { 13 | for i := 0; i < b.N; i++ { 14 | var sb StringBuf 15 | for j := 0; j < times; j++ { 16 | sb.Append(sample) 17 | } 18 | _ = sb.String() 19 | } 20 | } 21 | 22 | // strings.Builder 23 | func BenchmarkStringsBuilder_Append(b *testing.B) { 24 | for i := 0; i < b.N; i++ { 25 | var sb strings.Builder 26 | for j := 0; j < times; j++ { 27 | sb.WriteString(sample) 28 | } 29 | _ = sb.String() 30 | } 31 | } 32 | 33 | func BenchmarkStringBuf_Prepend(b *testing.B) { 34 | for i := 0; i < b.N; i++ { 35 | var sb StringBuf 36 | for j := 0; j < times; j++ { 37 | sb.Prepend(sample) 38 | } 39 | _ = sb.String() 40 | } 41 | } 42 | 43 | func BenchmarkStringsBuilder_PrependSimulated(b *testing.B) { 44 | for i := 0; i < b.N; i++ { 45 | result := "" 46 | for j := 0; j < times; j++ { 47 | result = sample + result 48 | } 49 | } 50 | } 51 | --------------------------------------------------------------------------------