Go add strings
last modified October 30, 2026
In this article we show how to concatenate strings in Go. A string is an immutable sequence of bytes; every concatenation creates a new string and leaves the operands unchanged.
Go offers several ways of adding strings:
- the
+and+=operators strings.Joinfor slices of stringsstrings.Builderfor incremental buildingbytes.Bufferfor byte-oriented buffersfmt.Sprintf,fmt.Sprintandfmt.Sprintlnfor formatted output- appending to a byte slice
All examples in this article were compiled and run with Go 1.27.0.
Adding strings with the + operator
The + operator concatenates two strings. It is the simplest way
to join a small, fixed number of strings.
package main
import "fmt"
func main() {
w1 := "an"
w2 := " old"
w3 := " falcon"
msg := w1 + w2 + w3
fmt.Println(msg)
}
The three words are joined into a new string, msg. The original
variables still hold their own values.
$ go run main.go an old falcon
Adding strings with the += operator
The compound assignment operator += appends the right operand to
the string in the left operand. It is shorthand for
msg = msg + value.
package main
import "fmt"
func main() {
msg := "There are"
msg += " three falcons"
msg += " in the sky"
fmt.Println(msg)
}
Each += statement creates a new, longer string and assigns it back
to msg.
$ go run main.go There are three falcons in the sky
Concatenating in a loop
Because strings are immutable, appending with += inside a loop
copies the whole string in every iteration. The total work grows quadratically
with the number of iterations, and so does the amount of garbage.
package main
import (
"fmt"
"strconv"
)
func main() {
msg := ""
for i := 0; i < 5; i++ {
msg += strconv.Itoa(i)
}
fmt.Println(msg)
}
The program produces the right result for five digits. For a large number of
iterations, strings.Builder or strings.Join are the
better choices.
$ go run main.go 01234
Joining strings with strings.Join
The strings.Join function concatenates the elements of a string
slice and places a separator between them. It allocates the result once.
package main
import (
"fmt"
"strings"
)
func main() {
words := []string{"There", "are", "two", "owls", "on", "the", "tree"}
msg := strings.Join(words, " ")
fmt.Println(msg)
}
The separator is the second argument. Here a single space is inserted between all words.
$ go run main.go There are two owls on the tree
Joining with a custom separator
The separator can be any string, including a multi-character string or a newline.
package main
import (
"fmt"
"strings"
)
func main() {
words := []string{"falcon", "sky", "earth"}
fmt.Println(strings.Join(words, " - "))
fmt.Println(strings.Join(words, "\n"))
}
The first call joins the words with a dash surrounded by spaces; the second one puts every word on its own line. No separator is added before the first element or after the last one.
$ go run main.go falcon - sky - earth falcon sky earth
Building strings with strings.Builder
The strings.Builder type accumulates text in an internal byte
buffer. WriteString appends without creating a new string for each
call, and String returns the accumulated text.
package main
import (
"fmt"
"strings"
)
func main() {
var builder strings.Builder
builder.WriteString("There")
builder.WriteString(" are")
builder.WriteString(" two")
builder.WriteString(" falcons")
builder.WriteString(" in")
builder.WriteString(" the")
builder.WriteString(" sky")
fmt.Println(builder.String())
fmt.Println("length:", builder.Len())
}
Len returns the number of bytes written so far. A
Builder must not be copied after its first use; pass a pointer to
it when you hand it to a function.
$ go run main.go There are two falcons in the sky length: 32
Formatted writes into a Builder
The fmt functions accept any io.Writer, and
*strings.Builder implements one. fmt.Fprintf
therefore formats values directly into the builder. Grow
pre-allocates space for the expected output.
package main
import (
"fmt"
"strings"
)
func main() {
var builder strings.Builder
builder.Grow(64)
for i := 1; i <= 3; i++ {
fmt.Fprintf(&builder, "%d. %s\n", i, "falcon")
}
fmt.Print(builder.String())
fmt.Println("bytes:", builder.Len())
}
The %d. %s\n format writes a numbered line for every iteration.
The reported size is the exact number of bytes in the builder.
$ go run main.go 1. falcon 2. falcon 3. falcon bytes: 30
Building a comma-separated list in a loop
This pattern writes a separator only between the elements, which avoids a
trailing comma. Converting the integers with strconv.Itoa keeps the
loop allocation-friendly.
package main
import (
"fmt"
"strconv"
"strings"
)
func main() {
var builder strings.Builder
builder.Grow(16)
nums := []int{2, 3, 5, 7}
for i, n := range nums {
if i > 0 {
builder.WriteString(", ")
}
builder.WriteString(strconv.Itoa(n))
}
fmt.Println(builder.String())
}
The builder holds the final text, and the single String call
returns it without copying the bytes again.
$ go run main.go 2, 3, 5, 7
Adding strings with bytes.Buffer
A bytes.Buffer is a variable-sized buffer of bytes with
WriteString, WriteByte and reading methods. Like
strings.Builder, it grows its storage as needed.
package main
import (
"bytes"
"fmt"
)
func main() {
var buffer bytes.Buffer
buffer.WriteString("a")
buffer.WriteString(" beautiful")
buffer.WriteString(" day")
fmt.Println(buffer.String())
fmt.Println("length:", buffer.Len())
}
The buffer holds the accumulated bytes; String interprets them as
a string.
$ go run main.go a beautiful day length: 15
Reading from a bytes.Buffer
Unlike strings.Builder, a bytes.Buffer can also be
read. ReadString returns the data up to and including the given
delimiter and removes it from the buffer.
package main
import (
"bytes"
"fmt"
)
func main() {
var buffer bytes.Buffer
buffer.WriteString("falcon sky earth")
word, err := buffer.ReadString(' ')
fmt.Printf("%q %v\n", word, err)
fmt.Println(buffer.String())
}
The first word is consumed from the buffer, so the remaining text is shorter.
A Builder cannot do this; it is write-only.
$ go run main.go "falcon " <nil> sky earth
Formatting with fmt.Sprintf
fmt.Sprintf builds a string from a format string and a list of
values. It returns the result instead of printing it.
package main
import "fmt"
func main() {
w1 := "an"
w2 := "old"
w3 := "falcon"
msg := fmt.Sprintf("%s %s %s", w1, w2, w3)
fmt.Println(msg)
n := 3
fmt.Println(fmt.Sprintf("There are %d falcons", n))
}
The %s verb expects a string and %d an integer. Mixing
text and numbers is where formatting is more readable than chains of
+.
$ go run main.go an old falcon There are 3 falcons
fmt.Sprint and fmt.Sprintln
fmt.Sprint concatenates the operands and adds spaces between
operands when neither of them is a string. fmt.Sprintln always
separates the operands with spaces and appends a newline.
package main
import "fmt"
func main() {
fmt.Println(fmt.Sprint("falcon", 3, true))
fmt.Println(fmt.Sprint(3, true, "falcon"))
fmt.Printf("%q\n", fmt.Sprintln("falcon", 3))
}
The last line prints the quoted result of Sprintln, which makes the
trailing \n visible.
$ go run main.go falcon3 true 3 truefalcon "falcon 3\n"
Appending to a byte slice
Strings can be assembled in a byte slice with append. The
... operator expands the string into its bytes.
package main
import "fmt"
func main() {
msg := make([]byte, 0, 16)
msg = append(msg, "an"...)
msg = append(msg, " old"...)
msg = append(msg, " falcon"...)
fmt.Println(string(msg))
fmt.Println("bytes:", len(msg))
}
The capacity passed to make avoids reallocations for this short
text. At the end, string converts the bytes back to a string. This
approach is close to what strings.Builder does internally and is
rarely needed directly.
$ go run main.go an old falcon bytes: 13
Repeating a string
To add the same string many times, use strings.Repeat. It returns
the string repeated the requested number of times.
package main
import (
"fmt"
"strings"
)
func main() {
fmt.Println(strings.Repeat("ab", 3))
fmt.Println(strings.Repeat("-", 10))
}
The second call creates a line of ten dashes. A count of zero returns an empty
string, while a negative count panics with
strings: negative Repeat count.
$ go run main.go ababab ----------
Strings are UTF-8 byte sequences
Concatenation works on bytes and does not interpret the encoding, so UTF-8 text can be joined directly. The byte length can differ from the number of characters.
package main
import (
"fmt"
"unicode/utf8"
)
func main() {
word := "żółw"
phrase := word + " " + "płynie"
fmt.Println(phrase)
fmt.Println(len(phrase), utf8.RuneCountInString(phrase))
}
The phrase has 11 runes but 15 bytes, because the Polish letters are encoded
with two bytes each. Use utf8.RuneCountInString when the number of
characters matters.
$ go run main.go żółw płynie 15 11
Source
Go strings package - reference
In this article we have shown how to add strings in Go with the
+ and += operators, strings.Join,
strings.Builder, bytes.Buffer, the
fmt formatting functions, and byte slices.
Author
List all Go tutorials.