Day 05: Arrays, Slices, and Strings
Arrays (Rarely Used Directly)
Arrays have a compile-time fixed length and are value types:
var a [5]int // [0 0 0 0 0]
b := [3]string{"x","y","z"}
c := [...]int{1,2,3} // compiler counts: [3]int
Because [3]int and [4]int are different types, functions usually accept slices instead.
Slices (The Idiomatic List)
A slice is a three-word header that describes a window into an underlying array:
┌─────────┬────────┬──────────┐
│ pointer │ length │ capacity │
└─────────┴────────┴──────────┘
│ │ │
│ │ └── how many elements the backing array can hold
│ └──────────── how many elements are currently visible
└────────────────────── where the window starts in the backing array
The backing array is allocated separately. Multiple slices can point to the same backing array (they share memory).
Creating slices
Slice literal — the most common way:
s := []int{10, 20, 30} // len=3, cap=3
make — preallocate when you know the size up front:
s := make([]int, 5) // len=5, cap=5 — all elements are 0
s := make([]int, 0, 10) // len=0, cap=10 — empty but pre-allocated
Using make([]T, 0, n) before a loop avoids repeated reallocations as you append.
nil slice — the zero value; safe to use immediately:
var s []int // len=0, cap=0, s == nil
s = append(s, 1) // fine — append handles nil correctly
A nil slice and an empty slice ([]int{}) behave identically with len, cap, and append. The only difference is s == nil.
append — adding elements
append adds elements to the end of a slice and always returns the (possibly new) slice. Always assign the result back.
s := []int{1, 2, 3}
s = append(s, 4) // [1 2 3 4]
s = append(s, 5, 6, 7) // [1 2 3 4 5 6 7] — multiple at once
Appending one slice to another — use the ... spread operator:
a := []int{1, 2, 3}
b := []int{4, 5, 6}
a = append(a, b...) // [1 2 3 4 5 6]
How reallocation works:
When append needs more space than cap allows, Go allocates a new, larger backing array (roughly doubling in size), copies all elements, and returns a slice pointing to the new array. The old backing array is abandoned.
s := make([]int, 0, 3)
fmt.Println(len(s), cap(s)) // 0 3
s = append(s, 1, 2, 3)
fmt.Println(len(s), cap(s)) // 3 3
s = append(s, 4) // cap exceeded — new backing array
fmt.Println(len(s), cap(s)) // 4 6 (or similar; exact growth factor varies)
Because reallocation returns a new slice, code like append(s, x) without reassigning s silently loses the new element. Always write s = append(s, x).
Iterating over a slice
range — the idiomatic way; gives index and value:
fruits := []string{"apple", "banana", "cherry"}
for i, v := range fruits {
fmt.Printf("[%d] = %s\n", i, v)
}
// [0] = apple
// [1] = banana
// [2] = cherry
Use _ to discard the index or value when you don’t need it:
for _, v := range fruits { // index not needed
fmt.Println(v)
}
for i := range fruits { // value not needed (Go 1.22+: also works for just the index)
fmt.Println(i)
}
Plain for loop — when you need finer control (e.g., skipping elements or iterating in reverse):
// Reverse iteration
for i := len(fruits) - 1; i >= 0; i-- {
fmt.Println(fruits[i])
}
rangegives a copy of each element. Modifyingvinside the loop does not change the slice. To modify elements in place, use the index:fruits[i] = "mango".
Slicing — creating sub-slices
The expression s[low:high] creates a new slice header pointing into the same backing array. No data is copied.
s := []int{0, 1, 2, 3, 4, 5}
a := s[1:4] // [1 2 3] — elements at index 1, 2, 3
b := s[:3] // [0 1 2] — from the start
c := s[3:] // [3 4 5] — to the end
d := s[:] // [0 1 2 3 4 5] — the whole slice
Because sub-slices share the backing array, modifying one is visible through the other:
a[0] = 99
fmt.Println(s) // [0 99 2 3 4 5] — s[1] changed!
Three-index slice s[low:high:max] limits the capacity of the result, preventing accidental writes through the sub-slice:
safe := s[1:4:4] // len=3, cap=3 — cannot grow into s[4], s[5]
copy — independent duplicate
copy(dst, src) copies elements from src to dst, returns the number copied, and the two slices are independent afterwards:
original := []int{1, 2, 3, 4, 5}
clone := make([]int, len(original))
copy(clone, original)
clone[0] = 99
fmt.Println(original) // [1 2 3 4 5] — unchanged
fmt.Println(clone) // [99 2 3 4 5]
copy copies min(len(dst), len(src)) elements. If dst is shorter, only len(dst) elements are copied.
Common in-place operations
Delete an element (order preserved):
s := []int{10, 20, 30, 40, 50}
i := 2 // delete index 2
s = append(s[:i], s[i+1:]...)
fmt.Println(s) // [10 20 40 50]
Delete an element (fast, order not preserved):
s[i] = s[len(s)-1] // overwrite target with last element
s = s[:len(s)-1] // trim the last slot
fmt.Println(s) // [10 20 50 40] — 50 moved to position 2
Insert an element at index i:
s := []int{10, 20, 30, 40}
i := 2
s = append(s[:i], append([]int{99}, s[i:]...)...)
fmt.Println(s) // [10 20 99 30 40]
Filter (keep only even numbers):
s := []int{1, 2, 3, 4, 5, 6}
result := s[:0] // re-use the backing array — no allocation
for _, v := range s {
if v%2 == 0 {
result = append(result, v)
}
}
fmt.Println(result) // [2 4 6]
len and cap
| Expression | Meaning |
|---|---|
len(s) |
Number of elements currently in the slice |
cap(s) |
Total elements the backing array can hold from s[0] |
len(s) == 0 |
Slice is empty (true for both nil and []int{}) |
s == nil |
Slice is nil (not merely empty) |
Strings
A string is a read-only []byte (UTF-8). len(s) is the byte count, not the character count.
s := "héllo"
len(s) // 6 (é is 2 UTF-8 bytes)
len([]rune(s)) // 5 (rune count)
for i, r := range s { // i = byte offset, r = rune
fmt.Printf("%d: %c (%d)\n", i, r, r)
}
Essential strings Functions
strings.Contains(s, "ell")
strings.HasPrefix(s, "he")
strings.HasSuffix(s, "lo")
strings.Count(s, "l")
strings.ToUpper(s) / strings.ToLower(s)
strings.TrimSpace(s)
strings.Split(s, ",")
strings.Join(parts, ", ")
strings.Replace(s, "old", "new", -1)
strings.Fields(s) // split on any whitespace
Building Strings Efficiently
var b strings.Builder
for _, word := range words {
b.WriteString(word)
b.WriteByte(' ')
}
result := b.String()
Concatenating with += in a loop is O(n²) — always use strings.Builder or strings.Join.
Runes
What is a rune?
rune is Go’s name for a single Unicode code point. It is an alias for int32:
type rune = int32 // built into the language — not defined in any package
A code point is the number Unicode assigns to each character. Every character in every human script has a unique code point, written as U+XXXX in hexadecimal:
| Character | Code point | Decimal |
|---|---|---|
A |
U+0041 | 65 |
é |
U+00E9 | 233 |
世 |
U+4E16 | 19990 |
😀 |
U+1F600 | 128512 |
A rune literal is written with single quotes:
var r rune = 'A' // r == 65
fmt.Println(r) // 65 (the integer value)
fmt.Printf("%c\n", r) // A (the character)
fmt.Printf("%U\n", r) // U+0041 (Unicode format)
Rune literals support the same escape sequences as strings: '\n' (newline), '\t' (tab), '\\' (backslash), '\'' (single quote).
How Go stores strings: UTF-8 encoding
A Go string is a sequence of bytes, not characters. The bytes are encoded as UTF-8, a variable-width encoding where each code point occupies 1 to 4 bytes:
| Code point range | Bytes used | Examples |
|---|---|---|
| U+0000 – U+007F | 1 byte | ASCII: A, 0, ! |
| U+0080 – U+07FF | 2 bytes | é, ñ, ü |
| U+0800 – U+FFFF | 3 bytes | 世, 界, € |
| U+10000 – U+10FFFF | 4 bytes | 😀, 🎉 |
This is why len(s) returns the byte count, which may be larger than the number of visible characters:
s := "héllo"
fmt.Println(len(s)) // 6 — é occupies 2 bytes (U+00E9 → 0xC3 0xA9)
fmt.Println(len([]rune(s))) // 5 — five visible characters
ASCII text is always 1 byte per character, so for pure ASCII strings len(s) equals the character count. Once you introduce non-ASCII characters, the counts diverge.
The classic mistake: indexing a string by position
Square-bracket indexing (s[i]) gives the byte at position i, not the character:
s := "héllo"
fmt.Println(s[0]) // 104 — byte value of 'h' (correct)
fmt.Println(s[1]) // 195 — first byte of 'é' (0xC3), not 'é'!
fmt.Printf("%c\n", s[1]) // Ã — wrong character
é is encoded as two bytes (0xC3, 0xA9). Indexing at position 1 lands in the middle of that sequence. To index by character (code point), convert to []rune first:
runes := []rune(s)
fmt.Printf("%c\n", runes[1]) // é — correct
Iterating over a string
Go provides two ways to iterate:
By byte — use a plain for loop with an index:
s := "héllo"
for i := 0; i < len(s); i++ {
fmt.Printf("byte[%d] = %d\n", i, s[i])
}
// byte[0] = 104
// byte[1] = 195 ← first byte of é
// byte[2] = 169 ← second byte of é
// byte[3] = 108 ← l
// byte[4] = 108 ← l
// byte[5] = 111 ← o
By rune (character) — use range:
for i, r := range s {
fmt.Printf("byte_offset[%d] = %c (U+%04X)\n", i, r, r)
}
// byte_offset[0] = h (U+0068)
// byte_offset[1] = é (U+00E9) ← i jumps from 1 to 3
// byte_offset[3] = l (U+006C)
// byte_offset[4] = l (U+006C)
// byte_offset[5] = o (U+006F)
range decodes the UTF-8 for you. i is the byte offset of the start of the rune, which is why it skips from 1 to 3 (because é took 2 bytes at positions 1 and 2). r is the full rune (the Unicode code point).
Converting between string, []byte, and []rune
s := "héllo"
b := []byte(s) // decode string into raw bytes — fast, no allocation for short strings
r := []rune(s) // decode string into runes (code points) — allocates
s2 := string(b) // encode bytes back to string
s3 := string(r) // encode runes back to string
// Single rune ↔ string
r1 := 'é'
fmt.Println(string(r1)) // "é"
fmt.Println([]byte(string(r1))) // [195 169] — the two UTF-8 bytes
When to use each:
| Need | Use |
|---|---|
| Raw byte manipulation (network, file) | []byte(s) |
| Character-level operations (count, index, reverse) | []rune(s) |
| Check or print a single character | rune |
Rune classification with the unicode package
The unicode package provides functions to classify runes:
package main
import (
"fmt"
"unicode"
)
func main() {
chars := []rune{'A', 'é', '3', ' ', '!', '世'}
for _, r := range chars {
fmt.Printf("%c letter=%-5v digit=%-5v space=%-5v upper=%-5v\n",
r,
unicode.IsLetter(r),
unicode.IsDigit(r),
unicode.IsSpace(r),
unicode.IsUpper(r),
)
}
}
Output:
A letter=true digit=false space=false upper=true
é letter=true digit=false space=false upper=false
3 letter=false digit=true space=false upper=false
letter=false digit=false space=true upper=false
! letter=false digit=false space=false upper=false
世 letter=true digit=false space=false upper=false
Quick reference
r := '世'
fmt.Println(r) // 19990 integer value
fmt.Printf("%c\n", r) // 世 character
fmt.Printf("%U\n", r) // U+4E16 Unicode code point
fmt.Printf("%d\n", r) // 19990 decimal
fmt.Println(string(r)) // 世 single-char string
fmt.Println(len(string(r)))// 3 bytes in UTF-8
s := "hello, 世界"
fmt.Println(len(s)) // 13 (7 ASCII + 3 + 3)
fmt.Println(len([]rune(s))) // 9 (9 characters)
Labs
Lab 1: Arrays — Fixed Size and Value Semantics
What you’ll practise: Declaring fixed-size arrays, comparing two arrays with ==, and observing copy semantics when an array is passed to a function.
Task:
Declare two [5]int arrays, compare them, pass one to a function that modifies it, and confirm the original is unchanged.
Steps:
- Declare
a := [5]int{1, 2, 3, 4, 5}andb := [5]int{1, 2, 3, 4, 5} - Compare with
a == b— arrays are comparable if their element type is - Write
zeroFirst(arr [5]int) [5]intthat setsarr[0] = 0and returns the modified copy - Call it and confirm the original
ais unchanged - Print
[3]intand[4]intare different types — try assigning one to the other
package main
import "fmt"
func zeroFirst(arr [5]int) [5]int {
arr[0] = 0
return arr
}
func main() {
a := [5]int{1, 2, 3, 4, 5}
b := [5]int{1, 2, 3, 4, 5}
c := [5]int{9, 2, 3, 4, 5}
fmt.Printf("a == b: %v\n", a == b)
fmt.Printf("a == c: %v\n", a == c)
modified := zeroFirst(a)
fmt.Printf("original a: %v\n", a)
fmt.Printf("modified: %v\n", modified)
// Arrays with different lengths are different types
// x := [3]int{1, 2, 3}
// a = x // compile error: cannot use [3]int as [5]int
fmt.Println("Arrays of different lengths are different types — cannot assign")
}
Expected output:
a == b: true
a == c: false
original a: [1 2 3 4 5]
modified: [0 2 3 4 5]
Arrays of different lengths are different types — cannot assign
Checkpoint: a is unchanged after calling zeroFirst — the function received a copy. Uncomment the assignment a = x and confirm the compile error message.
Lab 2: Slice Mechanics — len, cap, and Backing Arrays
What you’ll practise: Creating slices with make, understanding len vs cap, and observing when append allocates a new backing array.
Task:
Create a slice with make, append elements beyond its capacity, and use %p to detect when a new backing array is allocated.
Steps:
- Create
s := make([]int, 3, 5)— length 3, capacity 5 - Print
len,cap, and the pointer to the first element - Append two elements (still within capacity) — pointer should stay the same
- Append a sixth element (exceeds capacity) — pointer changes, new backing array allocated
- Show that a sub-slice shares the backing array
package main
import (
"fmt"
"unsafe"
)
func header(s []int) {
if len(s) == 0 {
fmt.Printf(" len=%-3d cap=%-3d ptr=<empty>\n", len(s), cap(s))
return
}
fmt.Printf(" len=%-3d cap=%-3d ptr=%p\n", len(s), cap(s), unsafe.SliceData(s))
}
func main() {
s := make([]int, 3, 5)
fmt.Println("after make([]int, 3, 5):")
header(s)
s = append(s, 10, 20) // still within capacity
fmt.Println("after append x2 (within cap):")
header(s)
s = append(s, 30) // exceeds capacity — new allocation
fmt.Println("after append x1 (exceeds cap — new backing array):")
header(s)
// Sub-slice shares backing array
sub := s[1:3]
fmt.Println("sub-slice s[1:3]:")
header(sub)
sub[0] = 999
fmt.Printf("s[1] after sub[0]=999: %d (shared!)\n", s[1])
}
Expected output:
after make([]int, 3, 5):
len=3 cap=5 ptr=0xc000...
after append x2 (within cap):
len=5 cap=5 ptr=0xc000... (same pointer)
after append x1 (exceeds cap — new backing array):
len=6 cap=10 ptr=0xc000... (different pointer)
sub-slice s[1:3]:
len=2 cap=9 ptr=0xc000...
s[1] after sub[0]=999: 999 (shared!)
Checkpoint: The pointer changes on the third append (when cap is exceeded). Modifying sub[0] changes s[1] — they share the same backing array until one of them appends past its capacity.
Lab 3: Slice Tricks
What you’ll practise: Using copy, building 2D slices, and performing common in-place slice operations (delete, insert).
Task:
Implement four classic slice manipulations from scratch, without importing any package beyond fmt.
Steps:
- Deep copy: use
copyto clone a slice and prove it is independent - Delete (unordered): swap the target element with the last, then trim — O(1)
- Delete (ordered): use
append(s[:i], s[i+1:]...)to preserve order — O(n) - Insert at index: use
appendtwice to insert a value in the middle
package main
import "fmt"
func main() {
// 1. Deep copy
original := []int{1, 2, 3, 4, 5}
clone := make([]int, len(original))
copy(clone, original)
clone[0] = 99
fmt.Printf("original: %v\nclone: %v\n\n", original, clone)
// 2. Delete index 2 — unordered (fast)
s := []int{10, 20, 30, 40, 50}
i := 2
s[i] = s[len(s)-1]
s = s[:len(s)-1]
fmt.Printf("delete unordered [2]: %v\n", s)
// 3. Delete index 1 — ordered (preserves order)
s = []int{10, 20, 30, 40, 50}
s = append(s[:1], s[2:]...)
fmt.Printf("delete ordered [1]: %v\n", s)
// 4. Insert 99 at index 2
s = []int{10, 20, 30, 40, 50}
s = append(s[:2], append([]int{99}, s[2:]...)...)
fmt.Printf("insert 99 at [2]: %v\n", s)
// 5. 2D slice (rows x cols)
matrix := make([][]int, 3)
for r := range matrix {
matrix[r] = make([]int, 4)
for c := range matrix[r] {
matrix[r][c] = r*4 + c
}
}
fmt.Println("3x4 matrix:", matrix)
}
Expected output:
original: [1 2 3 4 5]
clone: [99 2 3 4 5]
delete unordered [2]: [10 20 50 40]
delete ordered [1]: [10 30 40 50]
insert 99 at [2]: [10 20 99 30 40 50]
3x4 matrix: [[0 1 2 3] [4 5 6 7] [8 9 10 11]]
Checkpoint: clone[0] = 99 does not affect original[0]. Unordered delete does not preserve order (50 moves to position 2). Ordered delete does preserve order.
Lab 4: Strings as []rune — Unicode-Safe Iteration
What you’ll practise: Iterating a UTF-8 string with range to get runes (not bytes), counting bytes vs runes, and reversing a string correctly.
Task:
Work with a string that contains multi-byte UTF-8 characters, print each rune’s byte offset and Unicode value, then reverse the string correctly using []rune.
Steps:
- Use
rangeover"héllo, 世界"— print each byte offset and rune - Compare
len(s)(bytes) withlen([]rune(s))(runes) - Write
reverseString(s string) stringthat converts to[]rune, reverses, and converts back - Verify the reversal is correct for a multi-byte string
package main
import "fmt"
func reverseString(s string) string {
runes := []rune(s)
for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
runes[i], runes[j] = runes[j], runes[i]
}
return string(runes)
}
func main() {
s := "héllo, 世界"
fmt.Println("range iteration (byte offset → rune):")
for i, r := range s {
fmt.Printf(" byte[%2d]: %c (U+%04X)\n", i, r, r)
}
fmt.Printf("\nbyte count (len): %d\n", len(s))
fmt.Printf("rune count: %d\n", len([]rune(s)))
fmt.Printf("\noriginal: %s\n", s)
fmt.Printf("reversed: %s\n", reverseString(s))
}
Expected output:
range iteration (byte offset → rune):
byte[ 0]: h (U+0068)
byte[ 1]: é (U+00E9)
byte[ 3]: l (U+006C)
byte[ 4]: l (U+006C)
byte[ 5]: o (U+006F)
byte[ 6]: , (U+002C)
byte[ 7]: (U+0020)
byte[ 8]: 世 (U+4E16)
byte[11]: 界 (U+754C)
byte count (len): 13
rune count: 9
original: héllo, 世界
reversed: 界世 ,olléh
Checkpoint: Byte offset 1 jumps to 3 — é occupies bytes 1 and 2 (2-byte UTF-8 sequence). The reversed string is correct. Try reversing "hello" (ASCII) — same result as byte reversal.
Lab 5: strings Package — Builder and Common Operations
What you’ll practise: Using strings.Builder for efficient concatenation, and applying Fields, Split, Join, Replace, Contains, HasPrefix, and TrimSpace.
Task:
Use strings.Builder to join 1000 words efficiently, then apply six other strings functions to a sample sentence.
Steps:
- Use a
forloop +strings.Builderto concatenate 1000 “word” repetitions — measure withb.Len() - Apply
strings.Fields,strings.Split,strings.Join,strings.Replace,strings.Contains,strings.HasPrefix,strings.TrimSpaceto the sample sentence - Print results
package main
import (
"fmt"
"strings"
)
func main() {
// 1. strings.Builder — efficient concatenation
var b strings.Builder
for i := 0; i < 1000; i++ {
b.WriteString("word")
if i < 999 {
b.WriteByte(' ')
}
}
result := b.String()
fmt.Printf("Built string: %d bytes, starts with: %q\n\n", b.Len(), result[:20])
// 2. Common operations on a sentence
sentence := " The quick brown fox jumps over the lazy dog "
trimmed := strings.TrimSpace(sentence)
fmt.Println("TrimSpace: ", trimmed)
fmt.Println("Contains 'fox':", strings.Contains(trimmed, "fox"))
fmt.Println("HasPrefix 'The':", strings.HasPrefix(trimmed, "The"))
fmt.Println("Replace fox→cat:", strings.Replace(trimmed, "fox", "cat", 1))
words := strings.Fields(trimmed)
fmt.Printf("Fields: %d words, first=%q last=%q\n", len(words), words[0], words[len(words)-1])
parts := strings.Split(trimmed, " ")
joined := strings.Join(parts[:3], "-")
fmt.Println("Join first 3 with '-':", joined)
}
Expected output:
Built string: 4999 bytes, starts with: "word word word word "
TrimSpace: The quick brown fox jumps over the lazy dog
Contains 'fox': true
HasPrefix 'The': true
Replace fox→cat: The quick brown cat jumps over the lazy dog
Fields: 9 words, first="The" last="dog"
Join first 3 with '-': The-quick-brown
Checkpoint: Builder length is 4999 (4 chars per word × 1000 + 999 spaces). strings.Fields handles multiple consecutive spaces; strings.Split(s, " ") does not — try it with double spaces and compare.
Lab 6 (Final): String Statistics Tool
What you’ll practise: Combining slice iteration, rune handling, maps, and the strings package into a complete text analysis program.
Task:
Write day-05/main.go that analyses a hardcoded multi-line string and reports: rune count, byte count, word count, line count, longest word, and most frequent character.
Steps:
- Hardcode a multi-line string literal using a raw string literal (backticks)
- Count lines with
strings.Spliton"\n" - Count words with
strings.Fields - Count runes with
len([]rune(s)) - Find the longest word by iterating the word slice
- Build a
map[rune]intfor character frequency; find the max
package main
import (
"fmt"
"strings"
)
const text = `Go is an open source programming language that makes it easy
to build simple, reliable, and efficient software.
The Go programming language was conceived in late 2007.`
func main() {
// Byte and rune counts
byteCount := len(text)
runeCount := len([]rune(text))
// Lines
lines := strings.Split(strings.TrimSpace(text), "\n")
// Words
words := strings.Fields(text)
// Longest word
longest := ""
for _, w := range words {
if len(w) > len(longest) {
longest = w
}
}
// Most frequent rune (ignoring spaces)
freq := make(map[rune]int)
for _, r := range text {
if r != ' ' && r != '\n' {
freq[r]++
}
}
var topRune rune
var topCount int
for r, n := range freq {
if n > topCount {
topRune, topCount = r, n
}
}
fmt.Printf("Bytes: %d\n", byteCount)
fmt.Printf("Runes: %d\n", runeCount)
fmt.Printf("Lines: %d\n", len(lines))
fmt.Printf("Words: %d\n", len(words))
fmt.Printf("Longest word: %q (%d chars)\n", longest, len([]rune(longest)))
fmt.Printf("Top character: %q (appears %d times)\n", topRune, topCount)
}
Expected output:
Bytes: 152
Runes: 152
Lines: 3
Words: 29
Longest word: "programming" (11 chars)
Top character: 'e' (appears 10 times)
Checkpoint: Replace the text with one containing emoji or accented characters — confirm rune count differs from byte count. go vet . passes. Run go test ./... if you add a test file.
Day Project: String Statistics Tool
Write a program that takes a multi-line string (hardcoded) and computes:
- Total character count (runes)
- Total byte count
- Word count (using
strings.Fields) - Line count
- Longest word
- Most frequent character (use a
map[rune]int)
Extension ideas: read from os.Stdin; add sentence count; produce a frequency bar chart in the terminal.
Official Documentation
strings— Contains, Fields, Builder, Join, Split, ToUpper, TrimSpace, and moreunicode— rune classification (IsLetter, IsDigit, etc.)fmt— formatted I/O with%crune verbos—os.Stdinfor reading standard input- Language Spec: Slice types — slice internals
- Language Spec: String types — UTF-8 string representation
- Go Blog: Strings, bytes, runes and characters — deep dive on Go strings
- Go Tour: More types — slices and arrays tour