Go strings
A set of AI agent skills for research and development tasks.
npx -y skills add v0lka/skills --skill go-stringsAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 13 stars13 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Guides the agent to write correct and efficient Go string code. Use when writing or reviewing Go code that iterates over strings, concatenates strings, trims strings, converts between string and []byte, or extracts substrings. Covers rune semantics, string iteration pitfalls, trim vs. suffix/prefix functions, Builder-based concatenation, avoiding useless string/byte conversions, and preventing memory leaks from substrings.
SKILL.md
6.9 KB, as published. Nobody here has run it
Go Strings — Rules and Patterns
1. Runes, bytes, and len
- A Go
stringis an immutable sequence of bytes, not runes. len(s)returns the byte count, not the character/rune count.- A
runeis an alias forint32and represents a single Unicode code point. - Under UTF-8 a code point may occupy 1-4 bytes.
- To count runes, use
utf8.RuneCountInString(s).
s := "cafe\u0301" // "cafe" + combining accent = 5 runes, but check byte length
fmt.Println(len(s)) // byte count (likely 6)
fmt.Println(utf8.RuneCountInString(s)) // rune count (5)
2. String iteration
Prefer the range value variable for rune access
When ranging over a string, the index i is the byte offset of the rune, and the value r is the rune itself. Accessing s[i] gives a single byte, not the rune.
Wrong:
for i := range s {
fmt.Printf("%c", s[i]) // prints individual bytes, corrupts multi-byte runes
}
Correct — iterate over runes:
for i, r := range s {
fmt.Printf("position %d: %c\n", i, r) // i = byte offset, r = full rune
}
Accessing the i-th rune
If you need the rune at a logical index (not byte offset), convert to []rune first:
r := []rune(s)[4]
This allocates a new slice (O(n)), so avoid it in hot loops. If the string is known to
be pure ASCII, rune(s[i]) is safe and allocation-free.
3. Trim functions — TrimRight/TrimLeft vs. TrimSuffix/TrimPrefix
These are commonly confused. The semantics are fundamentally different:
| Function | Behaviour |
|---|---|
strings.TrimRight(s, cutset) | Removes all trailing runes that appear anywhere in cutset, one by one, until a rune not in the set is found. |
strings.TrimSuffix(s, suffix) | Removes the exact trailing suffix substring once. |
strings.TrimLeft(s, cutset) | Same as TrimRight but from the left. |
strings.TrimPrefix(s, prefix) | Same as TrimSuffix but from the left. |
strings.Trim(s, cutset) | Applies both TrimLeft and TrimRight. |
strings.TrimRight("123oxo", "xo") // "123" (strips o, x, o individually)
strings.TrimSuffix("123oxo", "xo") // "123o" (strips the suffix "xo" once)
Rule: When you want to remove a known fixed string from the start/end, use
TrimPrefix / TrimSuffix. When you want to strip a set of characters, use
TrimLeft / TrimRight / Trim.
4. String concatenation
Avoid += in loops
Strings are immutable. Every += allocates a new string and copies all previous
bytes. This is O(n^2) for n concatenations.
Wrong:
func concat(values []string) string {
s := ""
for _, v := range values {
s += v // new allocation every iteration
}
return s
}
Use strings.Builder in loops
func concat(values []string) string {
var sb strings.Builder
for _, v := range values {
sb.WriteString(v)
}
return sb.String()
}
Pre-grow the builder when total size is known or estimable
Computing the total byte length first and calling Grow avoids internal slice
re-allocations. Benchmarks show ~78% speedup over a non-pre-allocated Builder.
func concat(values []string) string {
total := 0
for _, v := range values {
total += len(v)
}
var sb strings.Builder
sb.Grow(total)
for _, v := range values {
sb.WriteString(v)
}
return sb.String()
}
When += is fine
For a small, fixed number of concatenations (roughly <= 5) or one-off formatting,
+=, fmt.Sprintf, or + are acceptable and more readable.
Safety note
strings.Builder is not safe for concurrent use.
5. Avoid useless string <-> []byte conversions
Each conversion between string and []byte allocates and copies because strings
are immutable.
Rule: If input arrives as []byte (from io.Reader, io.ReadAll, etc.) and the
result is also []byte, keep the entire pipeline in []byte. The bytes package
mirrors almost every function in strings:
strings function | bytes equivalent |
|---|---|
strings.TrimSpace | bytes.TrimSpace |
strings.Split | bytes.Split |
strings.Contains | bytes.Contains |
strings.Index | bytes.Index |
strings.Count | bytes.Count |
Wrong — double conversion:
func getBytes(reader io.Reader) ([]byte, error) {
b, err := io.ReadAll(reader)
if err != nil {
return nil, err
}
return []byte(strings.TrimSpace(string(b))), nil // 2 extra allocs
}
Correct — stay in []byte:
func getBytes(reader io.Reader) ([]byte, error) {
b, err := io.ReadAll(reader)
if err != nil {
return nil, err
}
return bytes.TrimSpace(b), nil // zero extra allocs
}
Guideline: Before reaching for the strings package, check whether the whole
workflow can use bytes instead.
6. Substrings and memory leaks
Substring expressions like s[a:b] produce a string that shares the same backing
array as the original. If the original string is large (e.g., a log line of thousands
of bytes) and you store only a small substring, the entire original backing array
stays in memory and cannot be garbage-collected.
Leaky:
uuid := log[:36] // uuid's backing array still holds the full log string
s.store(uuid)
Fix: copy the substring
Go >= 1.18 — use strings.Clone:
uuid := strings.Clone(log[:36]) // fresh 36-byte allocation
Pre-1.18 — manual deep copy:
uuid := string([]byte(log[:36])) // forces a new backing array
Note: some linters flag string([]byte(...)) as a "redundant conversion" — this
warning is incorrect in this context because the round-trip deliberately breaks
backing-array sharing.
Substring indexing caveat
s[a:b] operates on byte indices, not rune indices. For multi-byte runes,
convert to []rune first:
s := "Hêllo, World!"
sub := string([]rune(s)[:5]) // "Hêllo" — rune-safe
Quick-reference checklist
-
len(s)used only when byte count is intended;utf8.RuneCountInStringfor rune count. - String range loops use the value variable (
r), nots[i], for rune access. -
TrimRight/TrimLeftnot confused withTrimSuffix/TrimPrefix. - No
+=concatenation inside loops;strings.BuilderwithGrowpreferred. - No unnecessary
string<->[]byteround-trips;bytespackage used when possible. - Substrings of large strings copied via
strings.Clone(orstring([]byte(...))) before long-term storage.