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:
package main import "fmt" func main() { s := "" for i := range 5 { s += fmt.Sprint(i) + "," } fmt.Println(s) }
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:
package main import ( "fmt" "strings" ) func main() { tags := []string{"go", "backend", "tutorial"} fmt.Println(strings.Join(tags, ", ")) fmt.Println("#" + strings.Join(tags, " #")) }
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:
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") }
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:
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") }
43 bytes "GET /status HTTP/1.1\r\n" 21 bytes left
Which to use
| Situation | Use |
|---|---|
| A handful of pieces, once | + or fmt.Sprintf |
| Pieces already in a slice | strings.Join |
| Building text in a loop | strings.Builder |
| Building bytes, or writing then reading | bytes.Buffer |
| A larger document with placeholders | text/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:
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) } }
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.
n n² n³ ---------------- 1 1 1 2 4 8 3 9 27 4 16 64 5 25 125