Make you Go code work 1.5x faster OR even more

Michael Ushakov
3 min readOct 17, 2022

Introduction

Performance is a key thing of everything, we (humans) don’t like to wait and waste our time. Therefore, sometimes quick solution as most of the managers suppose are better than slow but with good engineering and design. But today we are not speaking about management, but about code performance. We have a small text formatting library that allows us to format text using a template in convenient on our view way. These not only formatting function do what fmt.Sprintf does in more convenient way but also provide an additional features. Previous versions of our module were loose in performance to fmt.Sprintf, but since 1.0.1 we are better. And today we are going to tell how to make any golang code work faster.

Parameters and return values

There are two options to pass either arguments and/or get function result — by pointer and, by value, consider the following example:

func getItemAsStr(item *interface{}) string   // 1st variant
func getItemAsStr(item interface{}) string // 2nd variant
func getItemAsStr(item *interface{}) *string // 3rd variant
func getItemAsStr(item interface{}) *string // 4th variant

According to our performance tests, we could conclude the following:

  1. We’ve got small performance rise when we pass arguments by pointer because we’ve got rid of arguments copying.
  2. We’ve got performance decrease when we return a pointer to function local variable

Therefore, the most optimal variant is the 1st variant.

Strings

Strings are immutable like in many other programming languages, therefore string concatenation using + operator is a bad idea, i.e. code like this is a very slow:

var result string = ""
for _, arg := range args {
result += getItemAsStr(&arg)
}

Every “+” creates a new string object as a result memory usage rise, performance significantly decreases due to spending sufficient time on allocations on new variables.

There is a better solution for string concat — use strings.Builder, but for a better performance you should initially enlarge buffer (using Grow function) to prevent it from some / all re-allocs, but if buffer size will be very large there will be a penalty to performance due to initial memory allocation will be slow, therefore you should choose initial buffer size wisely, i.e.:

var formattedStr = &strings.Builder{}
formattedStr.Grow(templateLen + 22*len(args))

Cycles

There are 2 ways to iterate over collection:

  • using range expression
for _, arg := range args {
// do some staff here
}
  • using traditional for with 3 expressions:
for i := start; i < templateLen; i++ {
// do some staff here
}

2nd variant is better by performance. But there are additional advantages of usage for with 3 expressions:

  • reduce number of iterations, more iteration code is slower, therefore, if you could affect your loop value, do it. This can’t be achieved with range;
  • set initial value for iteration much higher if it is possible, i.e. in our case:
start := strings.Index(template, "{")
if start < 0 {
return template
}
formattedStr.WriteString(template[:start])
for i := start; i < templateLen; i++ {
// iterate over i
}

Conclusion

Using such simple techniques we made our code run 1.5 times faster than it was, and now it works faster even than fmt.Sprintf, see our performance measurements:

. We also suggest you to use our library instead of fmt.Sprintf, because if you prepare strings for sql queries or something like this, it is important to make them as faster, as it possible. We would be thankful if you give us a star on Github and start to follow our organization here and on Github too.

--

--

Michael Ushakov

I am a scientist (physicist), an engineer (hardware & software) and the CEO of Wissance (wissance.com)