
Fluent string helpers for Go.
str wraps a Go string so cleanup and transformation steps can be chained from left to right. Method names follow the standard library where possible, and operations that count, slice, or pad text work in runes rather than bytes.
Installation
Requires Go 1.24 or newer.
go get github.com/goforj/str/v2Quick start
package main
import (
"fmt"
"github.com/goforj/str/v2"
)
func main() {
result := str.Of(" welcome_to_go ").Trim().Headline().String()
fmt.Println(result) // Welcome to Go
}API principles
str keeps the API deliberately small. These rules decide what belongs:
- Chains come first. Start with
str.Of. Methods that change text return a newstr.String, so the chain can continue. Checks, counts, parsers, and splits return ordinary Go values. - One job, one name. There are no aliases or compatibility shims. If two names mean the same thing, keep the clearer one.
- Use Go's words. When the standard library already names an operation, use the same name and argument order.
- Work in runes. Character positions, counts, slices, padding, and case changes handle Unicode text instead of UTF-8 bytes unless the method says otherwise.
- Do not hide edge cases. Empty searches do not match, replacing an empty search does nothing, and parsing or pattern errors are returned to the caller.
- Every method earns its place. It must solve a common application problem or make a chain meaningfully clearer. Narrow, project-specific rules belong elsewhere.
- Examples must keep working. Every public operation has a generated example, and the test suite runs each one and checks its output.
Why not just the standard library?
Often, you should. Go's strings, unicode, strconv, and regexp packages are the right choice when you only need one or two operations. This is already clear:
username := strings.ToLower(strings.TrimSpace(" GoForj_Admin "))
// goforj_adminThe same cleanup with str reads from left to right:
username := str.Of(" GoForj_Admin ").Trim().ToLower().String()
// goforj_adminEither version is reasonable. The difference is easier to see when more rules belong together.
Using the standard library:
func configKey(name string) string {
key := strings.TrimSpace(name)
key = strings.ToUpper(key)
key = strings.ReplaceAll(key, "-", "_")
key = strings.Trim(key, "_")
if !strings.HasPrefix(key, "APP_") {
key = "APP_" + key
}
return key
}
// configKey(" --billing-worker-- ") == "APP_BILLING_WORKER"Using str:
func configKey(name string) string {
return str.Of(name).
Trim().
ToUpper().
ReplaceAll("-", "_").
TrimChars("_").
EnsurePrefix("APP_").
String()
}
// configKey(" --billing-worker-- ") == "APP_BILLING_WORKER"Some jobs do not have a single standard library call. For example, Slug handles case, punctuation, repeated separators, and Unicode letters. It can be one step in a longer chain that turns a report title into a CSV filename with a 64-rune base name:
func exportFilename(reportTitle string) string {
return str.Of(reportTitle).
ReplaceAll("&", "and").
Slug().
Take(64).
TrimChars("-").
EnsurePrefix("report-").
EnsureSuffix(".csv").
String()
}
filename := exportFilename("Q3 Sales & Returns — North America")
// report-q3-sales-and-returns-north-america.csvstr uses the standard library underneath and has no dependencies. Use whichever version makes the rules easiest to see.
Performance
These comparisons measure equivalent standard-library and str operations. Each cell reports the median of 10 samples as ns/op · B/op · allocs/op.
Recorded with go1.26.1 on linux/arm64 using -cpu=1 (GOMAXPROCS=1).
| Workload | Standard library | str chain |
|---|---|---|
| Trim | 3.2 ns/op · 0 B/op · 0 allocs/op | 3.2 ns/op · 0 B/op · 0 allocs/op |
| ToLower | 61.2 ns/op · 32 B/op · 1 allocs/op | 63.0 ns/op · 32 B/op · 1 allocs/op |
| NormalizeSpace (Fields + Join) | 191.6 ns/op · 208 B/op · 2 allocs/op | 185.2 ns/op · 80 B/op · 1 allocs/op |
| Trim → ToLower | 71.8 ns/op · 32 B/op · 1 allocs/op | 69.3 ns/op · 32 B/op · 1 allocs/op |
| ReplaceAll × 3 | 136.6 ns/op · 96 B/op · 3 allocs/op | 133.1 ns/op · 96 B/op · 3 allocs/op |
Timing is machine-specific; use it to understand the scale of these operations, not as a universal speed claim. Treat small timing differences within the raw sample spread as noise. Allocation counts are less sensitive to machine speed and show how much heap work each composition performs. In these workloads, wrapping and unwrapping added no heap allocations; allocations came from transformations that produced new text. NormalizeSpace is algorithmically different: the standard-library composition builds a field slice before joining it, while str uses one builder pass.
The benchmark source and committed raw output record exactly what ran, including the Go version and command. Refresh the measurements explicitly with go -C docs run ./readme -record-benchmarks; ordinary README generation only renders that frozen snapshot.
API index
The full API and these examples are also available on pkg.go.dev.
API examples
These examples come from GoDoc and run as part of the test suite.
Affixes
EnsurePrefix
EnsurePrefix ensures the string starts with prefix, adding it if missing. Similar: EnsureSuffix and TrimPrefix.
v := str.Of("path/to").EnsurePrefix("/").String()
println(v)
// #string /path/toEnsureSuffix
EnsureSuffix ensures the string ends with suffix, adding it if missing. Similar: EnsurePrefix and TrimSuffix.
v := str.Of("path/to").EnsureSuffix("/").String()
println(v)
// #string path/to/TrimPrefix
TrimPrefix removes prefix when it appears at the start of the string. Similar: TrimSuffix and EnsurePrefix.
v := str.Of("https://goforj.dev").TrimPrefix("https://").String()
println(v)
// #string goforj.devTrimSuffix
TrimSuffix removes suffix when it appears at the end of the string. Similar: TrimPrefix and EnsureSuffix.
v := str.Of("file.txt").TrimSuffix(".txt").String()
println(v)
// #string fileUnwrap
Unwrap removes matching before and after strings if present. Similar: Wrap.
v := str.Of(`"GoForj"`).Unwrap(`"`, `"`).String()
println(v)
// #string GoForjWrap
Wrap surrounds the string with before and after. Similar: Unwrap.
v := str.Of("GoForj").Wrap(`"`, `"`).String()
println(v)
// #string "GoForj"Case
Camel
Camel converts the string to camelCase. Similar: Pascal.
v := str.Of("foo_bar baz").Camel().String()
println(v)
// #string fooBarBazHeadline
Headline converts the string into a human-friendly headline: splits on case/underscores/dashes/whitespace, title-cases words, and lowercases small words (except the first). Similar: Title.
v := str.Of("emailNotification_sent").Headline().String()
println(v)
// #string Email Notification SentKebab
Kebab converts the string to kebab-case. Similar: Snake.
v := str.Of("fooBar baz").Kebab().String()
println(v)
// #string foo-bar-bazLcFirst
LcFirst returns the string with the first rune lower-cased. Similar: UcFirst and ToLower.
v := str.Of("Gopher").LcFirst().String()
fmt.Println(v)
// #string gopherPascal
Pascal converts the string to PascalCase. Similar: Camel.
v := str.Of("foo_bar baz").Pascal().String()
fmt.Println(v)
// #string FooBarBazSnake
Snake converts the string to snake_case. Similar: Kebab.
v := str.Of("fooBar baz").Snake().String()
println(v)
// #string foo_bar_bazTitle
Title converts the string to title case (first letter of each word upper, rest lower) using Unicode rules. Similar: Headline.
v := str.Of("a nice title uses the correct case").Title().String()
println(v)
// #string A Nice Title Uses The Correct CaseToLower
ToLower returns a lowercase copy of the string using Unicode rules. Similar: ToUpper and LcFirst.
v := str.Of("GoLang").ToLower().String()
println(v)
// #string golangToUpper
ToUpper returns an uppercase copy of the string using Unicode rules. Similar: ToLower and UcFirst.
v := str.Of("GoLang").ToUpper().String()
println(v)
// #string GOLANGUcFirst
UcFirst returns the string with the first rune upper-cased. Similar: LcFirst and ToUpper.
v := str.Of("gopher").UcFirst().String()
println(v)
// #string GopherChecks
IsASCII
IsASCII reports whether the string consists solely of 7-bit ASCII runes.
v := str.Of("gopher").IsASCII()
println(v)
// #bool trueIsAlnum
IsAlnum reports whether the string contains at least one rune and every rune is a Unicode letter or number.
v := str.Of("Gopher2025").IsAlnum()
println(v)
// #bool trueIsAlpha
IsAlpha reports whether the string contains at least one rune and every rune is a Unicode letter.
v := str.Of("Gopher").IsAlpha()
println(v)
// #bool trueIsBlank
IsBlank reports whether the string contains only Unicode whitespace. Similar: IsEmpty.
v := str.Of(" \t\n")
println(v.IsBlank())
// #bool trueIsEmpty
IsEmpty reports whether the string has zero length. Similar: IsBlank.
v := str.Of("").IsEmpty()
println(v)
// #bool trueIsNumeric
IsNumeric reports whether the string contains at least one rune and every rune is a Unicode number.
v := str.Of("12345").IsNumeric()
println(v)
// #bool trueCleanup
Deduplicate
Deduplicate collapses consecutive instances of char into a single instance. If char is zero, space is used. Similar: NormalizeSpace.
v := str.Of("The Go Playground").Deduplicate(' ').String()
println(v)
// #string The Go PlaygroundNormalizeNewlines
NormalizeNewlines replaces CRLF, CR, and Unicode separators with \n. Similar: Lines.
v := str.Of("a\r\nb\u2028c").NormalizeNewlines().String()
println(v)
// #string a\nb\ncNormalizeSpace
NormalizeSpace removes surrounding whitespace and collapses internal whitespace to single spaces. Similar: Trim.
v := str.Of(" go forj ").NormalizeSpace().String()
println(v)
// #string go forjTrim
Trim removes leading and trailing Unicode whitespace. Similar: TrimLeft, TrimRight, and TrimChars.
v := str.Of(" GoForj ").Trim().String()
println(v)
// #string GoForjTrimChars
TrimChars removes leading and trailing runes contained in cutset. Similar: Trim.
v := str.Of("..GoForj!!").TrimChars(".!").String()
println(v)
// #string GoForjTrimLeft
TrimLeft removes leading Unicode whitespace. Similar: Trim and TrimRight.
v := str.Of(" GoForj ").TrimLeft().String()
println(v)
// #string GoForj\u0020\u0020TrimRight
TrimRight removes trailing Unicode whitespace. Similar: Trim and TrimLeft.
v := str.Of(" GoForj ").TrimRight().String()
println(v)
// #string \u0020\u0020GoForjComparison
EqualFold
EqualFold reports whether the string matches other using Unicode simple case folding.
v := str.Of("gopher").EqualFold("GOPHER")
println(v)
// #bool trueCompose
Append
Append concatenates the provided parts to the end of the string. Similar: Prepend.
v := str.Of("Go").Append("Forj", "!").String()
println(v)
// #string GoForj!Prepend
Prepend concatenates the provided parts to the beginning of the string. Similar: Append.
v := str.Of("World").Prepend("Hello ", "Go ").String()
println(v)
// #string Hello Go WorldConstructor
Of
Of wraps a raw string with fluent helpers.
v := str.Of("gopher")
println(v.String())
// #string gopherConversion
Bool
Bool parses the string as a bool using strconv.ParseBool semantics. Similar: Int and Float64.
v, err := str.Of("true").Bool()
println(v, err == nil)
// #bool true
// #bool trueFloat64
Float64 parses the string as a float64 using strconv.ParseFloat semantics. Similar: Bool and Int.
v, err := str.Of("3.14").Float64()
fmt.Println(v, err == nil)
// #float64 3.14
// #bool trueInt
Int parses the string as a base-10 int using strconv.Atoi semantics. Similar: Bool and Float64.
v, err := str.Of("42").Int()
println(v, err == nil)
// #int 42
// #bool trueEncoding
FromBase64
FromBase64 decodes a standard Base64 string. Similar: ToBase64.
v, err := str.Of("Z29waGVy").FromBase64()
println(v.String(), err == nil)
// #string gopher
// #bool trueToBase64
ToBase64 encodes the string using standard Base64. Similar: FromBase64.
v := str.Of("gopher").ToBase64().String()
println(v)
// #string Z29waGVyFluent
GoString
GoString allows %#v formatting to print the raw string.
v := str.Of("go")
println(fmt.Sprintf("%#v", v))
// #string goString
String returns the underlying raw string value.
v := str.Of("go").String()
println(v)
// #string goLength
RuneCount
RuneCount returns the number of Unicode code points in the string.
v := str.Of("gophers 🦫").RuneCount()
println(v)
// #int 9Masking
Mask
Mask replaces the middle of the string with the given rune, revealing revealLeft runes at the start and revealRight runes at the end. Negative reveal values count from the end. If the reveal counts cover the whole string, the original string is returned.
v := str.Of("gopher@example.com").Mask('*', 3, 4).String()
println(v)
// #string gop***********.comMatch
Match
Match reports whether the entire string matches pattern using [path.Match] syntax. A malformed pattern returns an error, and wildcards do not match a slash.
matched, err := str.Of("billing:reports").Match("billing:*")
println(matched, err == nil)
// #bool true
// #bool truePadding
PadBoth
PadBoth pads the string on both sides to reach length runes using pad (defaults to space). Widths at or below the current rune width leave the string unchanged. Similar: PadLeft and PadRight.
v := str.Of("go").PadBoth(6, "-").String()
println(v)
// #string --go--PadLeft
PadLeft pads the string on the left to reach length runes using pad (defaults to space). Widths at or below the current rune width leave the string unchanged. Similar: PadRight and PadBoth.
v := str.Of("go").PadLeft(5, " ").String()
println(v)
// #string \u0020\u0020\u0020goPadRight
PadRight pads the string on the right to reach length runes using pad (defaults to space). Widths at or below the current rune width leave the string unchanged. Similar: PadLeft and PadBoth.
v := str.Of("go").PadRight(5, ".").String()
println(v)
// #string go...Pluralize
Plural
Plural returns a best-effort English plural form of the final identifier word. It handles common English forms and identifier boundaries without claiming to resolve every irregular or ambiguous noun. Similar: Singular.
v := str.Of("city").Plural().String()
println(v)
// #string citiesSingular
Singular returns a best-effort English singular form of the final identifier word. It handles common English forms and identifier boundaries without claiming to resolve every irregular or ambiguous noun. Similar: Plural.
v := str.Of("people").Singular().String()
println(v)
// #string personReplace
Remove
Remove deletes all occurrences of provided substrings.
v := str.Of("The Go Toolkit").Remove("Go ").String()
println(v)
// #string The ToolkitReplaceAll
ReplaceAll replaces all occurrences of old with new in the string. If old is empty, the original string is returned unchanged.
v := str.Of("go gopher go").ReplaceAll("go", "Go").String()
println(v)
// #string Go Gopher GoReplaceArray
ReplaceArray replaces all occurrences of each old in olds with repl. Similar: ReplaceAll and Swap.
v := str.Of("The---Go---Toolkit")
println(v.ReplaceArray([]string{"---"}, "-").String())
// #string The-Go-ToolkitReplaceFirst
ReplaceFirst replaces the first occurrence of old with repl. Similar: ReplaceLast and ReplaceAll.
v := str.Of("gopher gopher").ReplaceFirst("gopher", "go").String()
println(v)
// #string go gopherReplaceFold
ReplaceFold replaces all non-overlapping occurrences of old with repl using Unicode simple case folding. An empty old string leaves the receiver unchanged. Similar: ReplaceAll.
v := str.Of("go gopher GO").ReplaceFold("GO", "Go").String()
println(v)
// #string Go Gopher GoReplaceLast
ReplaceLast replaces the last occurrence of old with repl. Similar: ReplaceFirst and ReplaceAll.
v := str.Of("gopher gopher").ReplaceLast("gopher", "go").String()
println(v)
// #string gopher goReplacePrefix
ReplacePrefix replaces old with repl when old is a prefix of the string. Similar: ReplaceSuffix and TrimPrefix.
v := str.Of("prefix-value").ReplacePrefix("prefix-", "new-").String()
println(v)
// #string new-valueReplaceSuffix
ReplaceSuffix replaces old with repl when old is a suffix of the string. Similar: ReplacePrefix and TrimSuffix.
v := str.Of("file.old").ReplaceSuffix(".old", ".new").String()
println(v)
// #string file.newSwap
Swap replaces multiple values using strings.Replacer built from a map. Similar: ReplaceArray.
pairs := map[string]string{"Gophers": "GoForj", "are": "is", "great": "fantastic"}
v := str.Of("Gophers are great!").Swap(pairs).String()
println(v)
// #string GoForj is fantastic!Search
Contains
Contains reports whether the string contains sub using a case-sensitive comparison. An empty substring is not a match. Similar: ContainsFold.
v := str.Of("Go means gophers").Contains("gopher")
println(v)
// #bool trueContainsFold
ContainsFold reports whether the string contains sub using Unicode simple case folding. An empty substring is not a match. Similar: Contains.
v := str.Of("Go means gophers").ContainsFold("GOPHER")
println(v)
// #bool trueCount
Count returns the number of non-overlapping occurrences of sub.
v := str.Of("gogophergo").Count("go")
println(v)
// #int 3HasPrefix
HasPrefix reports whether the string starts with prefix using a case-sensitive comparison. An empty prefix is not a match. Similar: HasPrefixFold and HasSuffix.
v := str.Of("gopher").HasPrefix("go")
println(v)
// #bool trueHasPrefixFold
HasPrefixFold reports whether the string starts with prefix using Unicode simple case folding. An empty prefix is not a match. Similar: HasPrefix and HasSuffixFold.
v := str.Of("gopher").HasPrefixFold("GO")
println(v)
// #bool trueHasSuffix
HasSuffix reports whether the string ends with suffix using a case-sensitive comparison. An empty suffix is not a match. Similar: HasSuffixFold and HasPrefix.
v := str.Of("gopher").HasSuffix("her")
println(v)
// #bool trueHasSuffixFold
HasSuffixFold reports whether the string ends with suffix using Unicode simple case folding. An empty suffix is not a match. Similar: HasSuffix and HasPrefixFold.
v := str.Of("gopher").HasSuffixFold("HER")
println(v)
// #bool trueIndex
Index returns the rune index of the first occurrence of sub, or -1 if not found. Similar: LastIndex.
v := str.Of("héllo").Index("llo")
println(v)
// #int 2LastIndex
LastIndex returns the rune index of the last occurrence of sub, or -1 if not found. Similar: Index.
v := str.Of("go gophers go").LastIndex("go")
println(v)
// #int 11Slug
Slug
Slug returns a lowercase Unicode slug separated by hyphens. Unicode letters and digits are preserved, while every other run is collapsed to one hyphen. Similar: Kebab.
v := str.Of("Go Forj Toolkit").Slug().String()
println(v)
// #string go-forj-toolkitSnippet
Excerpt
Excerpt returns a snippet around the first occurrence of needle with the given radius. If needle is not found, an empty string is returned. If radius <= 0, a default of 100 is used. Omission is used at the start/end when text is trimmed (default "...").
v := str.Of("This is my name").Excerpt("my", 3, "...")
println(v.String())
// #string ...is my na...Split
Lines
Lines splits the string into lines after normalizing newline variants. Similar: NormalizeNewlines.
v := str.Of("a\r\nb\nc").Lines()
fmt.Println(v)
// #[]string [a b c]Split
Split splits the string by the given separator.
v := str.Of("a,b,c").Split(",")
fmt.Println(v)
// #[]string [a b c]Substrings
After
After returns the substring after the first occurrence of sep. If sep is empty or not found, the original string is returned. Similar: AfterLast and Before.
v := str.Of("gopher::go").After("::").String()
println(v)
// #string goAfterLast
AfterLast returns the substring after the last occurrence of sep. If sep is empty or not found, the original string is returned. Similar: After and BeforeLast.
v := str.Of("pkg/path/file.txt").AfterLast("/").String()
println(v)
// #string file.txtBefore
Before returns the substring before the first occurrence of sep. If sep is empty or not found, the original string is returned. Similar: BeforeLast and After.
v := str.Of("gopher::go").Before("::").String()
println(v)
// #string gopherBeforeLast
BeforeLast returns the substring before the last occurrence of sep. If sep is empty or not found, the original string is returned. Similar: Before and AfterLast.
v := str.Of("pkg/path/file.txt").BeforeLast("/").String()
println(v)
// #string pkg/pathBetween
Between returns the substring between the first start marker and the first end marker after it. It returns an empty string when either marker is empty or missing.
v := str.Of("[first] and [second]").Between("[", "]").String()
println(v)
// #string firstCharAt
CharAt returns the rune at the given index and true if within bounds. Similar: Slice and RuneCount.
v, ok := str.Of("gopher").CharAt(2)
println(string(v), ok)
// #string p
// #bool trueCommonPrefix
CommonPrefix returns the longest shared prefix between the string and all provided others. Comparison is rune-safe. If no others are provided, the original string is returned. Similar: CommonSuffix.
v := str.Of("gopher").CommonPrefix("go", "gold").String()
println(v)
// #string goCommonSuffix
CommonSuffix returns the longest shared suffix between the string and all provided others. Comparison is rune-safe. If no others are provided, the original string is returned. Similar: CommonPrefix.
v := str.Of("main_test.go").CommonSuffix("user_test.go", "api_test.go").String()
println(v)
// #string _test.goLimit
Limit truncates the string to length runes, appending suffix if truncation occurs.
v := str.Of("Perfectly balanced, as all things should be.").Limit(10, "...").String()
println(v)
// #string Perfectly\u0020...Slice
Slice returns the substring between rune offsets [start:end). Indices are clamped; if start >= end the result is empty.
v := str.Of("naïve café").Slice(3, 7).String()
println(v)
// #string ve cSubstrReplace
SubstrReplace replaces the rune slice in [start:end) with repl.
v := str.Of("naïve café").SubstrReplace("i", 2, 3).String()
println(v)
// #string naive caféTake
Take returns the first length runes of the string (clamped). Similar: TakeLast and Limit.
v := str.Of("gophers").Take(3).String()
println(v)
// #string gopTakeLast
TakeLast returns the last length runes of the string (clamped). Similar: Take.
v := str.Of("gophers").TakeLast(4).String()
println(v)
// #string hersTransform
Repeat
Repeat repeats the string count times (non-negative).
v := str.Of("go").Repeat(3).String()
println(v)
// #string gogogoReverse
Reverse returns a rune-safe reversed string.
v := str.Of("naïve").Reverse().String()
println(v)
// #string evïanWords
FirstWord
FirstWord returns the first detected word or an empty string. Similar: LastWord and SplitWords.
v := str.Of("Hello world")
println(v.FirstWord().String())
// #string HelloInitials
Initials returns the uppercase first rune of each detected word. Words are split the same way as SplitWords, including camel case and acronym boundaries. Similar: SplitWords.
v := str.Of("portableNetwork graphics").Initials().String()
println(v)
// #string PNGJoin
Join concatenates elements with sep and returns the result to the fluent chain. The receiver provides fluent access and is not included in elements. Similar: Split.
v := str.Of("").Join([]string{"foo", "bar"}, "-").String()
println(v)
// #string foo-barLastWord
LastWord returns the last detected word or an empty string. Similar: FirstWord and SplitWords.
v := str.Of("Hello world").LastWord().String()
println(v)
// #string worldSplitWords
SplitWords splits the string into Unicode words, including camel case and acronym boundaries. Similar: FirstWord, LastWord, WordCount, and Words.
v := str.Of("one, two, three").SplitWords()
fmt.Println(v)
// #[]string [one two three]WordCount
WordCount returns the number of detected words. Similar: SplitWords.
v := str.Of("Hello, world!").WordCount()
println(v)
// #int 2Words
Words limits the string to count words, preserving the source through the selected word boundary and appending suffix if truncated. Similar: SplitWords and WrapWords.
v := str.Of("Perfectly balanced, as all things should be.").Words(3, " >>>").String()
println(v)
// #string Perfectly balanced, as >>>WrapWords
WrapWords wraps the string to the given rune width on whitespace boundaries, using breakStr between lines without discarding punctuation. Similar: Words.
v := str.Of("The quick brown fox jumped over the lazy dog.").WrapWords(20, "\n").String()
println(v)
// #string The quick brown fox\njumped over the lazy\ndog.Documentation
Development
docs and examples are separate Go modules, keeping their tooling and generated programs out of the library module download.
Run the tests and rebuild the generated examples and README with:
go test ./...
go -C docs test ./...
go -C examples test ./...
go -C docs run ./examplegen
go -C docs run ./readmeLicensed under the MIT License.
