5 Ways to Perform String Concatenation in Go
Strings are one of the building blocks of any programming language, and joining two or more strings together is an operation that you are sure to perform often. There are several possible methods for doing this in Go, each with their own unique trade-offs which will be considered in this article.
1. String concatenation using the + operator
The easiest way to concatenate strings in Go is by using the concatenation
operator (+
). Here’s an example:
func main() {
name := "John"
email := "john.doe@gmail.com"
s := name + "'s" + " email address is " + email
fmt.Println(s)
}
For basic string concatenation operations such as the one shown above, this should suffice. However, it can be inefficient for larger operations such when concatenating several large strings in a loop. Strings are immutable data structures, so each concatenation operation creates an entirely new string in memory.
2. Concatenating and formatting strings with fmt.Sprint()
Another way to concatenate strings in Go is by using the fmt.Sprint()
method
which is often more convenient than the +
operator since it can also
concatenate other non-string types.
func main() {
s1 := "John"
s2 := 20
s3 := fmt.Sprint("name:", s1, ", age:", s2)
fmt.Println(s3)
}
If you need greater control over the formatting of the string, you can use the
fmt.Sprintf()
method which allows you to specify a template format for the
resulting string:
func main() {
s1 := "John"
s2 := 20
s3 := fmt.Sprintf("[name]: %s; [age]: %d", s1, s2)
fmt.Println(s3)
}
When you want to interpolate variables in a string, you must use the fmt
verb that corresponds to the type of the
argument. For example, %s
is the verb for string, and %d
is for integers.
You also have a generic %v
for any type and several other formatting options.
3. Concatenating a string slice with strings.Join()
If you have a string slice, you can use strings.Join()
to create a single
string from the splice. You need to specify a separator string in its second
argument which is placed between each individual slice element in the resulting
string:
func main() {
s := []string
"a",
"quick",
"brown",
"fox",
"jumps",
"over",
"the",
"lazy",
"dog",
}
fmt.Println(strings.Join(s, " "))
fmt.Println(strings.Join(s, "_"))
}
4. Efficient string concatenation with bytes.Buffer
The bytes.Buffer
type was the go-to method for efficient string concatenation
in Go up until v1.10 when strings.Builder
was introduced (see the next
heading below) . Its zero value is an empty byte buffer that is ready to use
and it allows the manipulation of the buffer through the following methods on
the buffer:
func (b *Buffer) Write(p []byte) (int, error) // implementing io.Writer
func (b *Buffer) WriteByte(c byte) error
func (b *Buffer) WriteRune(r rune) (int, error)
func (b *Buffer) WriteString(s string) (int, error)
It’s really useful when iterating over some structure and building a string incrementally perhaps after some processing. The example below reads a file and builds a string from its contents:
func main() {
var buf bytes.Buffer
file, err := os.Open("hello.txt")
if err != nil {
log.Fatal(err)
}
scanner := bufio.NewScanner(file)
for scanner.Scan() {
text := strings.TrimSpace(scanner.Text())
text = strings.ToUpper(text) + " "
_, err := buf.WriteString(text)
if err != nil {
log.Fatal(err)
}
}
if err := scanner.Err(); err != nil {
log.Fatal(err)
}
fmt.Println(strings.TrimSpace(buf.String()))
}
Assuming the contents of hello.txt
is:
You’ll get the following output:
You should also explore other useful methods on
the bytes.Buffer
type such as:
Grow()
: for pre-allocating memory when the maximum size of the final result is known. Ensure to do this to derive maximum efficiency in such scenarios.Reset()
: For emptying the buffer while retaining the underline storage.
5. Efficient string concatenation with strings.Builder
The strings.Builder
type was introduced in Go v1.10 to replace bytes.Buffer
as the preferred way to efficiently concatenate strings in Go. It retains many
of the methods of its predecessor so its usage is quite similar in practice:
func main() {
var builder strings.Builder
file, err := os.Open("hello.txt")
if err != nil {
log.Fatal(err)
}
scanner := bufio.NewScanner(file)
for scanner.Scan() {
text := strings.TrimSpace(scanner.Text())
text = strings.ToUpper(text) + " "
_, err := builder.WriteString(text)
if err != nil {
log.Fatal(err)
}
}
if err := scanner.Err(); err != nil {
log.Fatal(err)
}
fmt.Println(strings.TrimSpace(builder.String()))
}
One thing to note when using strings.Builder
is that copying a populated
Builder
will lead to a panic! The recommended way to share a Builder
is to
use pointers.
func main() {
var b1 strings.Builder
b1.WriteString("abc")
b2 := b1
b2.WriteString("xyz")
}
Here’s how strings.Builder
differs from bytes.Buffer
:
- It only build strings and the strings are immutable.
- When you call
Reset()
on aBuilder
, the underlying storage is not retained unlikebytes.Buffer
. - You cant access its underlying bytes (like
(*Buffer).Bytes()
). - It does not implement the
io.Reader
interface.
Benchmarking string concatenation approaches in Go
Here’s a benchmark that you can run locally to see the performance difference between the five methods described above:
These are the results from running this benchmark on my machine with Go v1.19:
According to the results above, strings.Builder
was the fastest with the
fewest allocations (tied with strings.Join
), while bytes.Buffer
came in
third place. fmt.Sprint()
was by far the slowest with a massive amount of
allocations too due to its use of reflection while the concatenation operator
was just a little bit better.
So there you have it. The strings.Builder
type should be your go-to option
when performing large string concatenation operations, and its closest
alternatives (strings.Join()
and bytes.Buffer
) also have their place
depending on the scenario. However, fmt.Sprint()
and the +
operator should
be reserved for simple concatenation operations only.
Thanks for reading, and happy coding!