ZetCode

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:

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.

main.go
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.

main.go
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.

main.go
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.

main.go
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.

main.go
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.

main.go
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.

main.go
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.

main.go
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.

main.go
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.

main.go
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.

main.go
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.

main.go
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.

main.go
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.

main.go
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.

main.go
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

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Go tutorials.