Strings, runes and bytes: why len does not count characters, and a substring holds megabytes
A string in Go is a header of a pointer and a length over immutable bytes. Everything else follows: len gives bytes, s[i] gives a byte, range gives runes at byte offsets, a substring is free and therefore retains the whole original array. Measured: ten bytes hold eight megabytes, and joining a thousand pieces with += costs 109 times a Builder.
Full technical treatment
TL;DR
A string in Go is a sequence of bytes, not of characters. Text is stored in
UTF-8, where one mark takes between one and four bytes — so the length counts
bytes, indexing yields a byte, and for range walks code points. On purely
Latin text all three coincide, which is exactly why a bug from this topic lives
in the code until the first non-Latin input.
Hence what does not look the way people expect. "A世🙂" is eight bytes at
three runes: s[0] is 65 and that is the whole letter "A", while s[1] is 228
and that is a third of the CJK character — the first case works by accident
and thereby masks the second. range yields byte offsets 0 1 4, not 0 1 2.
Slicing cuts by bytes too: s[:2] splits the CJK character in half and produces
a string that is not valid UTF-8 — with no crash and no warning. And a rune is
not a character: a decomposed é is three bytes, two runes and one
visible mark, the family "👩👩👧" is eighteen bytes, five runes, one
mark; splitting into graphemes is not in the standard library at all.
Beyond that come the layout, the numbers and their limits. A string is a
header of a pointer and a length (16 bytes) over immutable bytes, and
both the free substring and the quadratic join follow from immutability. A
substring copies nothing — and for exactly that reason it retains the whole
original array: measured, ten bytes hold 8 MB until strings.Clone is
called. On go1.24 len([]rune(s)) allocates nothing — the compiler
recognises the expression, so the advice "replace it with
utf8.RuneCountInString" describes a compiler that has long since gone; you pay
where the slice is kept: ×3.4 and one allocation. For the same reason some
[]byte → string conversions are free: m[string(b)], string(b) == s,
range string(b) — zero allocations. And joining in a loop is quadratic: on
one machine the ratio against Builder grew with the number of pieces,
×2.3 → ×23.3 → ×111.8 — though at two or three pieces a + b + c is faster.
- text is held in memory as numbers: a byte is a number from 0 to 255;
- marks are not only Latin ones, and a single text can mix them freely;
- you take a string's length, cut a piece out of it by position, and walk it with a loop.
- UTF-8, code point, rune, grapheme cluster;
utf8.RuneCountInString,strings.Builder,strings.Clone, the replacement characterU+FFFD;- what a string variable is made of and why a substring copies no data.
What is really being asked
The topic looks like a warm-up, and that is the trap: the questions almost always come in pairs, where the second checks whether the reason behind the first was understood.
- "What does
len(s)return for a string with CJK characters in it?" — followed by: "and why?" - "What is a rune?" — followed by: "is a rune the same as a character?"
- "Why are strings immutable?" — a question about substrings, not about safety.
- "What does
s[2:5]cost?" — followed by: "and what stays in memory?" - "What is wrong with
+=in a loop?" — followed by: "and at three pieces?"
The topic has a single spine, and it is worth saying out loud: a string is a header over an immutable run of bytes. Immutability makes a substring free, a free substring retains the array, and immutability is also what makes joining quadratic. Three different questions, one property.
Base: a string is bytes
A string holds text, and memory can only hold numbers. So between the text and the memory sits an agreement: which number writes down which mark. Such an agreement is called an encoding, and in Go there is only one — UTF-8.
It is deliberately uneven: frequent marks are written in one byte, the rest in two, three or four. A Latin letter takes one byte, a CJK character three, an emoji four. The number the encoding assigns to a mark is called a code point; in Go it has its own name — a rune.
Take a string of three marks of different lengths — "A世🙂" — and walk the
whole entrance to the topic with it.
The length returns bytes. There are three marks, and the length is eight: one plus three plus four. A string's length in Go is not the number of marks but the room the text occupies.
Ranging walks marks. for range over a string steps through code points and
yields "A", "世" and "🙂" — three steps, not eight. But the number it hands you
alongside each mark is the byte offset where that mark starts: 0, 1 and 4.
Indexing yields a byte. s[0] is not the first mark but the first byte: 65,
which happens to be the whole letter "A", because that letter is one byte long.
And s[1] is 228, only the first third of the CJK character: the other two
thirds sit in s[2] and s[3].
That is the whole entrance. Three answers to "how many are there" — eight bytes, three marks, eight positions to index — and none of them is the string's "real" number: which one is right depends on why you are asking.
That is already enough to answer the basic interview question: a string's length in Go counts bytes rather than characters, because the text is stored in UTF-8 where a mark takes between one and four bytes. Everything below is about where that difference cuts: why a rune is still not a visible mark, how slicing a string splits a mark in half, and what follows from what a string variable is made of.
Mechanism 1: one string, three frames of reference
The lesson carries that same string — "A世🙂" — through every mechanism. It is
chosen to cover all three UTF-8 lengths at once: the Latin letter takes one byte, the CJK
character three, the emoji four.
This is not pedantry. On a purely Latin string not one bug in this topic
shows up: len matches the number of characters, s[0] gives the first
character, range indices run consecutively. Which is exactly why such bugs
live in the code until the first non-Latin input — and surface in production
rather than in tests.
A run of bench/gostring/practice.go prints:
8 len(s) — bytes
3 utf8.RuneCountInString(s) — runes
65 s[0] — a byte, and also the whole letter "A"
228 s[1] — a byte, and it is a THIRD of the CJK character
3 len([]rune(s))
0 1 4 byte offsets from range
1 len(string(s[0]))
2 len(string(s[1]))
Three points are what matter here.
len counts bytes. Eight against three is one and the same string in two
frames of reference, and neither of them is the "real" one. The right answer to
"how many characters are there" depends on why you are asking.
s[i] is a byte, not a character. And here is the pair the string was
chosen for: s[0] gives 65, and that really is the whole letter "A"; s[1]
gives 228, and that is the first of three bytes. The first case works by
accident — and thereby masks the second.
The last two output lines follow from that. string(s[i]) does not decode the
byte: it converts a number into the rune with that code. From 65 comes "A",
1 byte long — right by coincidence. From 228 comes "ä", 2 bytes long — a
character with no relation to the original string.
range yields byte offsets: 0 1 4, not 0 1 2. That is in the
specification directly:
For a string value, the "range" clause iterates over the Unicode code points in
the string starting at byte index 0. On successive iterations, the index value
will be the index of the first byte of successive UTF-8-encoded code points.
The practical consequence: an index from range is fit for slicing the
string (s[i:]) but is not a character number. A character counter goes in
a separate variable.
Mechanism 2: bytes, runes and graphemes — three different numbers
"A rune is a character" is the most durable simplification in the topic, and it
is worth demolishing right away, before it works its way into decisions. A run of
bench/gostring/internals.go:
| string | bytes | runes | visible marks |
|---|---|---|---|
"A世🙂" | 8 | 3 | 3 |
"é" precomposed | 2 | 1 | 1 |
"é" decomposed | 3 | 2 | 1 |
"👩👩👧" a family | 18 | 5 | 1 |
The two middle rows are the same mark on screen with different rune counts.
é exists in two forms: precomposed (U+00E9) and decomposed (e plus a
combining acute U+0301). So a rune is not a character, and a rune count answers
"how many characters" correctly only when the text is known to be in the
precomposed form — which text from the outside world does not guarantee.
The last row takes the distinction to its limit: eighteen bytes, five runes, one visible mark. Those five runes are three emoji and two zero-width joiners.
What a person calls a character is called a grapheme cluster, and splitting
into clusters is not in the standard library at all. That is the substantive
interview answer: "count the characters" in the general case is solved by a
separate package, while len and []rune answer two other questions.
The three levels are worth holding in mind as three:
bytes the unit of storage len(s)
runes Unicode code points utf8.RuneCountInString(s)
graphemes what a person sees a separate package
Mechanism 3: slicing cuts by bytes
String slice indices are byte indices, which means s[:n] with an arbitrary
n can cut a UTF-8 sequence in half. From the run:
| expression | result | valid UTF-8 | runes |
|---|---|---|---|
s[:1] | "A" | true | 1 |
s[:2] | "A\xe4" | false | 2 |
s[:4] | "A世" | true | 2 |
s[:8] | "A世🙂" | true | 3 |
The second row is a CJK character cut in half. The program did not crash and said nothing: a string in Go is a sequence of arbitrary bytes, and invalid UTF-8 inside one is legal.
Note the rune counter on that row: it counted two runes where the second does
not exist. Iteration substitutes the replacement character U+FFFD for every
invalid byte — so the corruption turns not into an error but into a question mark
in a diamond that users will notice.
The practical consequence is narrow and common: you cannot truncate text by
length with a byte slice. A hundred-character preview cut as s[:100] will
sooner or later split a character. Cut on the boundaries range gives — it walks
exactly the starts of runes.
Mechanism 4: what is in the variable — and why a substring is free
The measurement prints the header sizes:
| size | |
|---|---|
string | 16 bytes (pointer + length) |
[]byte | 24 bytes (pointer + length + capacity) |
Those eight bytes of difference explain half the topic: a string has no capacity, because nothing can ever be appended to it.
A string value is a (possibly empty) sequence of bytes. Strings are immutable:
once created, it is impossible to change the contents of a string.
Hence, immediately: s[0] = 'x' does not compile, and that is not caution
for its own sake. Writing into a string would corrupt every substring looking
into the same array — and they look into it precisely because a string is
immutable. The property holds itself up.
A substring is free — and therefore expensive
Here is the pair they want to hear together.
The substring s[10:20] costs zero allocations: a new header points into the
same array. And for exactly that reason:
| left on the heap | |
|---|---|
| an 8 MB string created | 8.0 MB |
| kept a 10-byte substring of it | 8.0 MB |
after strings.Clone | 0.0 MB |
Ten bytes retain eight megabytes. The collector reclaims an array whole or not at all; a reference into the array holds all of it.
Where this actually happens: a large service response is parsed, an identifier is taken out of it and put into a long-lived cache — and the response stays in memory along with the identifier. The symptom is characteristic: memory grows while the object count shows nothing.
Clone returns a fresh copy of s. It guarantees to make a copy of s into a new
allocation, which can be important when retaining only a small substring of a
much larger string.
The rule that follows is a narrow one: strings.Clone is needed only where a
short piece outlives a long string. Cloning everything means paying in copies
for a problem you do not have.
Mechanism 5: joining in a loop — why this is not about the speed of an operation
Before looking at the numbers it is worth deriving the answer from what has
already been said. A string is immutable — that is the lesson's first claim.
So "appending to a string" is impossible in principle: every += builds a new
string and copies everything accumulated so far into it. On step k it copies k
pieces, and over n steps the sum from 1 to n — the work is quadratic.
The prediction from that is unambiguous: the ratio against Builder must
grow with the number of pieces. The growth, not the ratio itself: a single
number says nothing, because it depends on the length of the loop.
| pieces | s += x | Builder | Builder+Grow | ratio |
|---|---|---|---|---|
| 10 | 474 ns | 208 ns | 71 ns | ×2.3 |
| 100 | 23,938 ns | 1,028 ns | 502 ns | ×23.3 |
| 1000 | 1,674,914 ns | 14,976 ns | 4,279 ns | ×111.8 |
The prediction holds: ×2.3 → ×23.3 → ×111.8. A growing ratio is the signature of quadratic work; if the work were linear, the column would stand still.
Allocations at a thousand pieces:
999 against 15 for Builder and 1 for Builder with Grow.
Builder writes into a growing buffer and hands it over as a string without a
copy; Grow removes the intermediate expansions as well, when the final size is
known in advance.
And the caveat without which the rule does harm. At two or three pieces
a + b + c is faster than a Builder and reads better: the compiler joins
such an expression with a single call. A Builder is for where the pieces are
many and their number is not known in advance — that is, in a loop.
Deeper: conversions — what copies and what does not
From here the claims change kind, and the difference is worth saying. The
contract says only that string and []byte are different types; everything
below is what the current compiler does, and numbers taken on one
particular machine.
This is where the topic carries its most outdated piece of common wisdom.
First. len([]rune(s)) — the very expression people are told to replace with
utf8.RuneCountInString — allocates nothing. Measured:
| time | allocations | |
|---|---|---|
utf8.RuneCountInString(s) | 98.83 ns | 0 |
len([]rune(s)) | 104.26 ns | 0 |
r := []rune(s), slice kept | 333.39 ns | 1 |
The compiler recognises the shape len([]rune(s)) and counts runes in place
without building a slice. The advice repeated in articles describes a compiler
that has long since gone. You pay where the slice is actually needed — when
it is kept and indexed.
Second. Some []byte → string conversions are free:
| allocations | |
|---|---|
m[string(b)] — a map lookup | 0 |
string(b) == s — a comparison | 0 |
for range string(b) | 0 |
s := string(b) — the string is kept | 1 |
The rule is simple: a copy is needed when the string outlives the
expression. If it is needed only inside one expression and never escapes, the
compiler does not build it. Hence the practical consequence: looking up a
map[string]T by a key held in a []byte can be done directly, with no
unsafe and no manual workarounds.
How to answer in an interview
Short answer: a string in Go is a sequence of bytes in UTF-8, not a sequence
of characters. That is why the length counts bytes, indexing yields a byte,
and for range walks code points and hands back byte offsets. Add the layout as
a second sentence: a string is a pointer and a length over immutable bytes,
and both the free substring and the quadratic join follow from immutability.
That is enough for a correct answer. What follows is what you add when the interviewer digs.
If the interviewer digs deeper
On len, answer with a number and a reason. "Bytes. For "A世🙂" it is
eight against three runes: the letter takes one byte, the CJK character three,
the emoji four."
On s[i], name the type and give both halves. "byte. In that same string
s[0] is 65 and the whole letter "A", while s[1] is 228 and a third of the CJK
character. And string(s[1]) gives not the character but an unrelated "ä": it
converts a number into the rune with that code rather than decoding the byte."
On characters, separate the three levels. "Bytes, runes and graphemes are
three different numbers. A decomposed é is two runes for one visible mark, an
emoji family five runes for one. Splitting into graphemes is not in the standard
library."
On runes, add the second half. "A rune is a code point, but not a character
on screen: é may be two runes, and then the counter shows two for one visible
mark."
On substrings, name both sides at once. "Zero allocations — and retention of
the whole array. Ten bytes held eight megabytes for me; strings.Clone fixes
it."
On joining, talk about growth, not about the ratio. "Quadratic: every +=
copies what has accumulated. At a thousand pieces it came out ×109 against a
Builder. But at two or three pieces the plus is faster."
Next they ask
How does []byte(s) differ from an unsafe conversion?
[]byte(s) copies — and must: a slice is mutable, a string is not. Without
the copy, writing into the slice would change the string that other substrings
may be looking at.
unsafe.String and unsafe.Slice (Go 1.20) do the same without a copy, and
that is exactly why they are dangerous: the resulting slice looks into the
string's memory, and any write to it is undefined behaviour. Their legitimate
place is a hot path where you know for certain the data is only read. In an
interview answer, what matters is naming not the trick but the condition under
which it applies.
How do you reverse a string properly?
A trick question, and the right answer starts with one of your own: reverse
what? Bytes cannot be reversed — UTF-8 would break. Runes can, via []rune,
and that is what people mean in 99% of cases.
But even that is not "reversing a string": combining marks come loose from their
base characters, and an é made of two runes turns into an acute over the
neighbouring letter. A fully correct reversal requires splitting into grapheme
clusters — not something the standard library does.
What does strings.Builder do that bytes.Buffer does not?
Both accumulate into a growing buffer; the difference is at the end.
Builder.String() hands the buffer over without a copy — it can afford to,
because it forbids copying the Builder itself (the noCopy check panics).
Buffer.String() copies, because the buffer may keep being used afterwards.
Hence the choice: if the result is a string and the buffer is no longer needed,
Builder is cheaper by exactly one copy. If you need an io.Writer you can
also read from, use a Buffer.
Why does comparing strings not require equal lengths?
Comparison looks at the lengths first: different means false immediately,
without a single byte compared. Equal lengths mean the bytes are compared, and
for identical pointers the runtime returns at once.
The practical consequence: comparing strings is cheap at different lengths and only gets expensive on matching prefixes of equal length. This is also why strings are legitimate map keys: the hash is computed over the bytes, and the comparison is unambiguous because the bytes are immutable.
Can a string hold invalid UTF-8?
Yes, and that is an important yes: a string is a sequence of arbitrary bytes. Nothing stops you from putting something that is not UTF-8 into one — data read from a file, for instance.
What happens then: range and []rune substitute U+FFFD, the replacement
character, for every invalid byte, while utf8.ValidString returns false. In
other words, corruption does not crash the program; it quietly turns into
question marks in diamonds, and only a check will find it.
Where do string literals live?
In the read-only data segment of the executable — which is another way to explain immutability: writing into a literal would be a write into a page marked read-only.
Hence a small but useful point: s := "abc" allocates nothing on the heap, and
neither does a substring of a literal, while []byte("abc") does — because a
slice is mutable and cannot be left in that segment.
Common misconceptions
len(s) returns the number of characters
The number of bytes. For "A世🙂" that is 8 against three runes: the letter takes one byte, the CJK character three, the emoji four. The rune count comes from utf8.RuneCountInString, and even that is not the number of visible characters: a decomposed é is two runes for one mark.
s[0] is the first character of the string
It is the first byte, of type byte. For "A世🙂" it is 65 and coincides with the whole letter "A" — while s[1] is 228 and only a third of the CJK character. And string(s[1]) gives not «世» but an unrelated «ä»: it converts a number into the rune with that code, rather than decoding the byte.
range over a string gives the indices 0, 1, 2…
It gives the byte offsets of the start of each rune: for "A世🙂" those are 0, 1, 4. An index from range is fit for slicing the string and unfit as a character number — the counter is kept separately.
a rune is a character
A rune is one Unicode code point. A visible character may consist of several: é is written either as one point or as a letter plus a combining acute, and in the second case there are two runes for one mark on screen. Counting characters is a grapheme-cluster problem.
a substring copies the data, so it is expensive
The opposite: zero allocations — a new header points into the same array. What is expensive is the other side: a substring retains the whole original array. Measured: ten bytes hold 8 MB until strings.Clone is called.
len([]rune(s)) allocates, so replace it with utf8.RuneCountInString
The measurement does not support this: len([]rune(s)) makes zero allocations and takes the same time — the go1.24 compiler recognises the expression and counts runes in place. That is a property of the compiler, not a promise of the language: the specification says nothing about such conversions. The allocation appears when the slice is kept: 300.84 ns against 94.52, and one allocation.
converting []byte to string always copies
Only when the string outlives the expression. m[string(b)], string(b) == s and for range string(b) make zero allocations — the go1.24 compiler knows the string will not escape. On another implementation the copy would be legitimate: the contract promises only that string and []byte are different types.
never concatenate with +, always use a Builder
At two or three pieces a + b + c is faster than a Builder and reads better — the compiler joins such an expression with a single call. A Builder is for where the pieces are many and their number is unknown in advance: at a thousand pieces the gap is ×109, and it grows with their number, because the work is quadratic.
Practice
Two problems. Answer first, then check against the real output: in both, the correct answer comes from a run of the script, not from an assertion.
Practice · predict the output
fmt.Println(len(s))
fmt.Println(utf8.RuneCountInString(s))
fmt.Println(s[0])
fmt.Println(s[1])
fmt.Println(len([]rune(s)))
var offsets []string
for i := range s {
offsets = append(offsets, strconv.Itoa(i))
}
fmt.Println(strings.Join(offsets, " "))
fmt.Println(len(string(s[0])))
fmt.Println(len(string(s[1])))Practice · estimate
Knowledge check
s := «A世🙂». What will fmt.Println(len(s), s[0], s[1]) print?
This is neither a retelling nor a separate text: everything below is taken from the article itself — its own summary, the section headings, the “actually” column and the version table. Which is why these theses cannot drift from the article.
The gist
- A string in Go is a sequence of bytes, not of characters. Text is stored in UTF-8, where one mark takes between one and four bytes — so the length counts bytes, indexing yields a byte, and
for rangewalks code points. On purely Latin text all three coincide, which is exactly why a bug from this topic lives in the code until the first non-Latin input. - Hence what does not look the way people expect.
"A世🙂"is eight bytes at three runes:s[0]is 65 and that is the whole letter "A", whiles[1]is 228 and that is a third of the CJK character — the first case works by accident and thereby masks the second.rangeyields byte offsets0 1 4, not0 1 2. Slicing cuts by bytes too:s[:2]splits the CJK character in half and produces a string that is not valid UTF-8 — with no crash and no warning. And a rune is not a character: a decomposedéis three bytes, two runes and one visible mark, the family"👩👩👧"is eighteen bytes, five runes, one mark; splitting into graphemes is not in the standard library at all. - Beyond that come the layout, the numbers and their limits. A string is a header of a pointer and a length (16 bytes) over immutable bytes, and both the free substring and the quadratic join follow from immutability. A substring copies nothing — and for exactly that reason it retains the whole original array: measured, ten bytes hold 8 MB until
strings.Cloneis called. On go1.24len([]rune(s))allocates nothing — the compiler recognises the expression, so the advice "replace it withutf8.RuneCountInString" describes a compiler that has long since gone; you pay where the slice is kept: ×3.4 and one allocation. For the same reason some[]byte→stringconversions are free:m[string(b)],string(b) == s,range string(b)— zero allocations. And joining in a loop is quadratic: on one machine the ratio againstBuildergrew with the number of pieces, ×2.3 → ×23.3 → ×111.8 — though at two or three piecesa + b + cis faster.
In fact
- The number of bytes. For
"A世🙂"that is 8 against three runes: the letter takes one byte, the CJK character three, the emoji four. The rune count comes fromutf8.RuneCountInString, and even that is not the number of visible characters: a decomposedéis two runes for one mark. - It is the first byte, of type
byte. For"A世🙂"it is 65 and coincides with the whole letter "A" — whiles[1]is 228 and only a third of the CJK character. Andstring(s[1])gives not «世» but an unrelated «ä»: it converts a number into the rune with that code, rather than decoding the byte. - It gives the byte offsets of the start of each rune: for
"A世🙂"those are 0, 1, 4. An index from range is fit for slicing the string and unfit as a character number — the counter is kept separately. - A rune is one Unicode code point. A visible character may consist of several:
éis written either as one point or as a letter plus a combining acute, and in the second case there are two runes for one mark on screen. Counting characters is a grapheme-cluster problem. - The opposite: zero allocations — a new header points into the same array. What is expensive is the other side: a substring retains the whole original array. Measured: ten bytes hold 8 MB until
strings.Cloneis called. - The measurement does not support this:
len([]rune(s))makes zero allocations and takes the same time — the go1.24 compiler recognises the expression and counts runes in place. That is a property of the compiler, not a promise of the language: the specification says nothing about such conversions. The allocation appears when the slice is kept: 300.84 ns against 94.52, and one allocation. - Only when the string outlives the expression.
m[string(b)],string(b) == sandfor range string(b)make zero allocations — the go1.24 compiler knows the string will not escape. On another implementation the copy would be legitimate: the contract promises only thatstringand[]byteare different types. - At two or three pieces
a + b + cis faster than a Builder and reads better — the compiler joins such an expression with a single call. A Builder is for where the pieces are many and their number is unknown in advance: at a thousand pieces the gap is ×109, and it grows with their number, because the work is quadratic.
What is covered
- What is really being asked
- Base: a string is bytes
- Mechanism 1: one string, three frames of reference
- Mechanism 2: bytes, runes and graphemes — three different numbers
- Mechanism 3: slicing cuts by bytes
- Mechanism 4: what is in the variable — and why a substring is free
- Mechanism 5: joining in a loop — why this is not about the speed of an operation
- Deeper: conversions — what copies and what does not
- How to answer in an interview
- Next they ask
- Common misconceptions
- Practice
- Knowledge check
Sources & further reading
4 SOURCES
- The Go Programming Language Specification — String typesOfficial documentation. The definition everything else is derived from: «A string value is a (possibly empty) sequence of bytes. The number of bytes is called the length of the string and is never negative. Strings are immutable: once created, it is impossible to change the contents of a string.» Note the wording: a sequence of bytes, not of characters.https://go.dev/ref/spec#String_types
- The Go Programming Language Specification — For statements with range clauseOfficial documentation. The answer to why range indices come with gaps: «For a string value, the "range" clause iterates over the Unicode code points in the string starting at byte index 0. On successive iterations, the index value will be the index of the first byte of successive UTF-8-encoded code points in the string.»https://go.dev/ref/spec#For_range
- Strings, bytes, runes and characters in Go — the Go blogOfficial documentation. Rob Pike's article, the source of the precise vocabulary: «a rune is a Go term for a single Unicode code point» and, more importantly, the warning against equating a rune with a character: «Code points can be represented by multiple runes» — that is, combining marks.https://go.dev/blog/strings
- Package strings — Builder and CloneOfficial documentation. The two tools of this lesson. On Builder: «A Builder is used to efficiently build a string using Write methods. It minimizes memory copying.» On Clone, which speaks to the retention trap directly: «Clone returns a fresh copy of s. It guarantees to make a copy of s into a new allocation, which can be important when retaining only a small substring of a much larger string.»https://pkg.go.dev/strings