Strings

A Go string is a read-only run of bytes; len counts bytes, indexing gives one byte, and every change makes a new string.

Syntaxs := "text"; n := len(s); b := s[0]; bs := []byte(s); back := string(bs)

A string is a piece of text, written between double quotes: "hello". Underneath, Go stores it as a run of bytes. A byte is one number from 0 to 255. Plain English letters take one byte each, so for them the count of bytes and the count of characters agree. Accented letters and other scripts take two, three or four bytes, and then they do not.

len counts bytes

len(s) gives the number of bytes in s. len("hello") is 5, but len("héllo") is 6, because the é takes two bytes. Indexing with s[0] gives you one byte, as a number, not a character. Printing name[0] of "Göteborg" shows 71, which is the code for G. The next lesson covers how to walk the characters instead.

Strings do not change

A string is read-only. s[0] = 'A' is a compile error. Every operation that looks like a change actually builds a new string and leaves the old one alone. strings.ToUpper(s) returns a new string. s + t returns a new string holding both. s += t is shorthand for s = s + t; they are the same operation written two ways. To change bytes in place, convert to []byte(s), which copies them into something writable, then convert back with string(b).

Two kinds of literal

Double quotes read backslash escapes: "a\nb" holds a newline, and "\\" holds one backslash. Backticks take every character exactly as typed, newlines included, which suits file paths and blocks of SQL or HTML.

Example

Example · go
package main

import (
	"fmt"
	"strings"
)

func main() {
	name := "Göteborg"

	// len reports bytes, not characters. The ö takes two bytes.
	fmt.Println("bytes:", len(name))

	// Indexing gives one byte, which prints as a number.
	fmt.Println("first byte:", name[0])
	// %c asks fmt to print that number as the character it stands for.
	fmt.Printf("first byte as text: %c\n", name[0])

	// Nothing changes a string. ToUpper returns a new one.
	fmt.Println(strings.ToUpper(name), "and", name)

	// += is shorthand for the line under it. Both build a new string.
	greeting := "hello"
	greeting += " there"
	greeting = greeting + " again"
	fmt.Println(greeting)

	// Backticks keep every character as typed, backslashes included.
	fmt.Println(`C:\temp\new`)
}

// Output:
// bytes: 9
// first byte: 71
// first byte as text: G
// GÖTEBORG and Göteborg
// hello there again
// C:\temp\new

When to use it

  • Trim and split one line of a config file into a key and a value.
  • Build a long SQL statement in a strings.Builder instead of gluing it with +.
  • Hand text to a file or a network connection, which both want []byte.
  • Write a Windows path or a block of HTML in backticks so no backslash needs escaping.

More examples

Joining text without copying it over and over

Because + makes a whole new string each time, gluing many pieces together with + copies everything repeatedly. strings.Builder keeps one buffer that grows, and hands over the finished string at the end without copying it again. Use + for two or three pieces and a Builder when the number of pieces is not known in advance.

Example · go
package main

import (
	"fmt"
	"strings"
)

func main() {
	// Every + produces a brand new string, copying both sides into it.
	a := "user"
	b := "@example.com"
	email := a + b
	fmt.Println(email)

	// A Builder appends into one growing buffer instead.
	var sb strings.Builder
	sb.WriteString("user")
	sb.WriteString("@")
	sb.WriteString("example.com")
	fmt.Println(sb.String())

	// Len is how many bytes the Builder has collected so far.
	fmt.Println(sb.Len(), len(email))
}

// Output:
// [email protected]
// [email protected]
// 16 16

Changing bytes by copying to []byte and back

[]byte(s) copies the string's bytes into a writable list of bytes, and string(bs) copies them back into a new read-only string. Both conversions copy, which is exactly why neither can alter the original. This is the pattern for any byte-level edit, and it is also how you hand text to anything that wants bytes, such as a file or a network connection.

Example · go
package main

import "fmt"

func main() {
	s := "abc"

	// []byte(s) copies the bytes somewhere you are allowed to write.
	bs := []byte(s)
	bs[0] = 'A'

	// string(bs) copies them back into a new read-only string.
	fmt.Println(s, string(bs))

	// s[0] = 'A' would not compile. The original never moved.
	fmt.Printf("%d %c\n", s[0], s[0])
}

// Output:
// abc Abc
// 97 a

Trimming, searching and splitting one line

These four calls cover most text handling. TrimSpace cleans both ends. Contains answers yes or no, Index gives the byte position or -1 when absent. Cut, added in Go 1.18, splits once at the first match and reports whether it found the separator at all, which is what you want for parsing a header or a key and value.

Example · go
package main

import (
	"fmt"
	"strings"
)

func main() {
	line := "  name: Ada Lovelace  "

	// TrimSpace removes spaces, tabs and newlines from both ends.
	line = strings.TrimSpace(line)
	// %q prints the string with quotes, so you can see the edges.
	fmt.Printf("%q\n", line)

	// Contains answers yes or no. Index gives a byte position, or -1.
	fmt.Println(strings.Contains(line, "name:"), strings.Index(line, ":"))

	// Cut splits once, at the first match, and says whether it found it.
	before, after, found := strings.Cut(line, ": ")
	fmt.Println(found, before, after)
}

// Output:
// "name: Ada Lovelace"
// true 4
// true name Ada Lovelace

Discussion

  • Be the first to comment on this lesson.