Building Strings

Joining strings with + is fine for a few pieces, but because strings are immutable, each + copies everything built so far — in a loop that adds up fast. This lesson shows the efficient tools: strings.Join, strings.Builder, bytes.Buffer and fmt.Fprintf, and finishes with text/template for filling in larger blocks of text.

The problem with += in a loop

Each s += piece creates a brand-new string and copies the old contents into it. For 10,000 pieces that's 10,000 ever-larger copies. It works, but it does far more work than necessary:

GO
package main

import "fmt"

func main() {
	s := ""
	for i := range 5 {
		s += fmt.Sprint(i) + ","
	}
	fmt.Println(s)
}
Output
0,1,2,3,4,

strings.Join

If you already have the pieces in a slice, strings.Join works out the total size, allocates once and copies each piece once. It also puts the separator only between items, avoiding the trailing comma above:

GO
package main

import (
	"fmt"
	"strings"
)

func main() {
	tags := []string{"go", "backend", "tutorial"}
	fmt.Println(strings.Join(tags, ", "))
	fmt.Println("#" + strings.Join(tags, " #"))
}
Output
go, backend, tutorial
#go #backend #tutorial

strings.Builder

A strings.Builder collects text in a growing buffer and turns it into a string once, at the end. Its zero value is ready to use:

GO
package main

import (
	"fmt"
	"strings"
)

func main() {
	var b strings.Builder
	for i := 1; i <= 3; i++ {
		if i > 1 {
			b.WriteString(" | ")
		}
		b.WriteString("item ")
		b.WriteByte('#')
		fmt.Fprintf(&b, "%02d", i)
	}
	b.WriteRune('✓')

	fmt.Println(b.String())
	fmt.Println(b.Len(), "bytes")
}
Output
item #01 | item #02 | item #03✓
33 bytes

fmt.Fprintf(&b, …) formats straight into the builder, because a *strings.Builder is an io.Writer — anything that can be written to (lesson 58). If you know roughly how big the result will be, call b.Grow(n) first to allocate once. Don't copy a Builder after you've started writing to it; pass a pointer instead.

bytes.Buffer

bytes.Buffer is similar, but it works with bytes and can be read from as well as written to. Use it when the result is binary data or will be passed on as bytes, such as an HTTP request body:

GO
package main

import (
	"bytes"
	"fmt"
)

func main() {
	var buf bytes.Buffer
	buf.WriteString("GET /status HTTP/1.1\r\n")
	buf.WriteString("Host: example.com\r\n\r\n")
	fmt.Println(buf.Len(), "bytes")

	line, _ := buf.ReadString('\n') // reading consumes from the front
	fmt.Printf("%q\n", line)
	fmt.Println(buf.Len(), "bytes left")
}
Output
43 bytes
"GET /status HTTP/1.1\r\n"
21 bytes left

Which to use

SituationUse
A handful of pieces, once+ or fmt.Sprintf
Pieces already in a slicestrings.Join
Building text in a loopstrings.Builder
Building bytes, or writing then readingbytes.Buffer
A larger document with placeholderstext/template or html/template

Templates

For emails, reports, config files or code generation, text/template fills a template with data. {{.Field}} inserts a value, and {{range}} and {{if}} add loops and conditions. For HTML pages, use html/template, which has the same syntax and also escapes values to prevent cross-site scripting:

GO
package main

import (
	"os"
	"text/template"
)

type Order struct {
	Customer string
	Items    []string
	Express  bool
}

const receipt = `Hello {{.Customer}},
Your order:
{{range $i, $item := .Items}}  {{$i}}. {{$item}}
{{end}}{{if .Express}}Shipping: express
{{else}}Shipping: standard
{{end}}`

func main() {
	t := template.Must(template.New("receipt").Parse(receipt))
	order := Order{"Ana", []string{"Notebook", "Pens"}, true}
	if err := t.Execute(os.Stdout, order); err != nil {
		panic(err)
	}
}
Output
Hello Ana,
Your order:
  0. Notebook
  1. Pens
Shipping: express

template.Must panics if the template has a syntax error, which is appropriate for templates written into the program. Execute writes to any io.Writer: standard output here, but equally a file, a strings.Builder or an HTTP response.

Try it yourself

Build a simple text table of squares and cubes with a strings.Builder.

Try it yourself · main.go
Edit the code, copy it into main.go in a new folder and run it with go run main.go (Go 1.27 or later). How to install Go
Output
  n    n²     n³
----------------
  1     1      1
  2     4      8
  3     9     27
  4    16     64
  5    25    125
Next: pattern matching with regular expressions.