In Golang should I work with []bytes or strings

In go, all that you can do with strings, you also can do with []bytes (compare, contains, replace, concat)

I’ve done some benchmark here (you can also find at the end of this article), to know which is the faster :

  • compare (strings are faster)
BenchmarkBytesCompare-4   300000000  5.06 ns/op 0 B/op 0 allocs/op
BenchmarkStringsCompare-4 1000000000 2.37 ns/op 0 B/op 0 allocs/op
  • contains (strings are faster)
BenchmarkBytesContains-4   100000000 17.2 ns/op 0 B/op 0 allocs/op
BenchmarkStringsContains-4 100000000 12.3 ns/op 0 B/op 0 allo
  • Index (strings are faster)
BenchmarkBytesIndex-4  200000000 8.75 ns/op 0 B/op 0 allocs/op
BenchmarkStringIndex-4 200000000 6.83 ns/op 0 B/op 0 allocs/op
  • Replace (bytes are faster, and allocate less object)
BenchmarkBytesReplace-4   20000000 77.3 ns/op 32 B/op 1 allocs/op
BenchmarkStringsReplace-4 10000000 137 ns/op 64 B/op 2 allocs/op
  • Concat (bytes are faster), I’ve done this test, thinking in the most useful case, because on internet I often see a concat in a loop, doing tons of concat, but it’s rare that in my go application I do more than 4 concats.
BenchmarkBytesConcat-4   50000000 39.4 ns/op 32 B/op 1 allocs/op
BenchmarkStringsConcat-4 30000000 51.0 ns/op 32 B/op 1 allocs/op
BenchmarkStringsJoin-4 20000000 85.0 ns/op 64 B/op 2 allocs/op

Conclusion

  • Strings are faster for searches (contains, index, compare) purpose.
  • bytes are faster in create (replace, concat) purpose.

Anyway, a lot of functions of input/output (io) prefers the bytes, and the conversion cost :

b:= string(bytes)BenchmarkBytesToStrings-4  30000000  38.6 ns/op 32 B/op 1 allocs/op

So my advice, if you have in input a []byte continue working with it. If you have a string continue working with it. (when it’s possible of course)

Except if you have to do a lot of comparison, or a lot of concatenation. On this case it could be better to convert.

And on all case do a benchmark. because all depend on your code.

Hints

The compiler have some optimization for bytes. So if you are working with maps prefer :

value, ok := mymap[string(mybytes)]BenchmarkMapHints-4 100000000 12.3 ns/op 0 B/op 0 allocs/op

than

key := string(mybytes)
value, ok := mymap[key]
BenchmarkMapsHints_Dont-4 100000000 22.0 ns/op 0 B/op 0 allocs/op

Code

package mainimport (
"bytes"
"strings"
"testing"
)
var result interface{}//BenchmarkBytesToStrings convert a []bytes in a string
//https://golang.org/pkg/bytes/#Compare
func BenchmarkBytesToStrings(b *testing.B) {
s1 := []byte("string to convert")b.ResetTimer()
b.ReportAllocs()
var r string
for n := 0; n < b.N; n++ {
r = string(s1)
}
result = r
}
//BenchmarkBytesCompare compare 2 []bytes.
//https://golang.org/pkg/bytes/#Compare
func BenchmarkBytesCompare(b *testing.B) {
s1 := []byte("string to compare")
s2 := []byte("string to compare")
b.ResetTimer()
b.ReportAllocs()
var r int
for n := 0; n < b.N; n++ {
r = bytes.Compare(s1, s2)
}
result = r
}
//BenchmarkStringsCompare compare 2 strings.
//https://golang.org/pkg/strings/#Compare
func BenchmarkStringsCompare(b *testing.B) {
s1 := "string to compare"
s2 := "string to compare"
b.ResetTimer()
b.ReportAllocs()
var r int
for n := 0; n < b.N; n++ {
r = strings.Compare(s1, s2)
}
result = r
}
//BenchmarkBytesContains check contains method
//https://golang.org/pkg/bytes/#Contains
func BenchmarkBytesContains(b *testing.B) {
s1 := []byte("string to compare")
s2 := []byte("comparc")
b.ResetTimer()
b.ReportAllocs()
var r bool
for n := 0; n < b.N; n++ {
r = bytes.Contains(s1, s2)
}
result = r
}
//BenchmarkStringsContains check contains method
//https://golang.org/pkg/strings/#Contains
func BenchmarkStringsContains(b *testing.B) {
s1 := "string to compare"
s2 := "comparc"
b.ResetTimer()
b.ReportAllocs()
var r bool
for n := 0; n < b.N; n++ {
r = strings.Contains(s1, s2)
}
result = r
}
//BenchmarkBytesIndex check contains index
//https://golang.org/pkg/bytes/#Index
func BenchmarkBytesIndex(b *testing.B) {
s1 := []byte("string to compare")
s2 := []byte("e")
b.ResetTimer()
b.ReportAllocs()
var r int
for n := 0; n < b.N; n++ {
r = bytes.Index(s1, s2)
}
result = r
}
//BenchmarkStringIndex check contains index
//https://golang.org/pkg/strings/#Index
func BenchmarkStringIndex(b *testing.B) {
s1 := "string to compare"
s2 := "e"
b.ResetTimer()
b.ReportAllocs()
var r int
for n := 0; n < b.N; n++ {
r = strings.Index(s1, s2)
}
result = r
}
//BenchmarkBytesReplace check replace method
//https://golang.org/pkg/bytes/#Replace
func BenchmarkBytesReplace(b *testing.B) {
s1 := []byte("string to comparc")
s2 := []byte("comparc")
s3 := []byte("compare")
b.ResetTimer()
b.ReportAllocs()
var r []byte
for n := 0; n < b.N; n++ {
r = bytes.Replace(s1, s2, s3, -1)
}
result = r
}
//BenchmarkStringsReplace check replace method
//https://golang.org/pkg/strings/#Replace
func BenchmarkStringsReplace(b *testing.B) {
s1 := "string to comparc"
s2 := "comparc"
s3 := "compare"
b.ResetTimer()
b.ReportAllocs()
var r string
for n := 0; n < b.N; n++ {
r = strings.Replace(s1, s2, s3, -1)
}
result = r
}
//BenchmarkBytesConcat concats 2 bytes
func BenchmarkBytesConcat2(b *testing.B) {
s1 := []byte("string to compare")
s2 := []byte("string to add")
b.ResetTimer()
b.ReportAllocs()
for n := 0; n < b.N; n++ {
r := make([]byte, 0, len(s1)+len(s2))
r = append(r, s1...)
r = append(r, s2...)
}
}
//BenchmarkStringsConcat concats 2 strings
func BenchmarkStringsConcat2(b *testing.B) {
s1 := "string to compare"
s2 := "string to add"
b.ResetTimer()
b.ReportAllocs()
var r string
for n := 0; n < b.N; n++ {
r = s1 + s2
}
result = r
}
//BenchmarkStringsJoin joins 2 strings
//https://golang.org/pkg/strings/#Join
func BenchmarkStringsJoin2(b *testing.B) {
s1 := "string to compare"
s2 := "string to add"
b.ResetTimer()
b.ReportAllocs()
var r string
for n := 0; n < b.N; n++ {
j := []string{s1, s2}
r = strings.Join(j, "")
}
result = r
}
//BenchmarkMapHints bench mymap[string(abytes)]
func BenchmarkMapHints(b *testing.B) {
mymap := make(map[string]string)
mymap["key"] = "value"
abytes := []byte("key")
b.ResetTimer()
b.ReportAllocs()
var v string
for n := 0; n < b.N; n++ {
v, _ = mymap[string(abytes)]
}
result = v
}
//BenchmarkMapsHints_Dont bench key := string(abytes)
//v, _ = mymap[key]
func BenchmarkMapsHints_Dont(b *testing.B) {
mymap := make(map[string]string)
mymap["key"] = "value"
abytes := []byte("key")
b.ResetTimer()
b.ReportAllocs()
var v string
for n := 0; n < b.N; n++ {
key := string(abytes)
v, _ = mymap[key]
}
result = v}

To test :

go test bench=.

Links :

https://github.com/Tkanos/strings-vs-bytes

Lead engineer at Mindgeek