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.
Requires Go 1.24 or newer.
go get github.com/goforj/str/v2package 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
}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.
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.
The full API and these examples are also available on pkg.go.dev.
These examples come from GoDoc and run as part of the test suite.
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 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 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 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 removes matching before and after strings if present. Similar: Wrap.
v := str.Of(`"GoForj"`).Unwrap(`"`, `"`).String()
println(v)
// #string GoForjWrap surrounds the string with before and after. Similar: Unwrap.
v := str.Of("GoForj").Wrap(`"`, `"`).String()
println(v)
// #string "GoForj"Camel converts the string to camelCase. Similar: Pascal.
v := str.Of("foo_bar baz").Camel().String()
println(v)
// #string fooBarBazHeadline 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 converts the string to kebab-case. Similar: Snake.
v := str.Of("fooBar baz").Kebab().String()
println(v)
// #string foo-bar-bazLcFirst returns the string with the first rune lower-cased. Similar: UcFirst and ToLower.
v := str.Of("Gopher").LcFirst().String()
fmt.Println(v)
// #string gopherPascal converts the string to PascalCase. Similar: Camel.
v := str.Of("foo_bar baz").Pascal().String()
fmt.Println(v)
// #string FooBarBazSnake converts the string to snake_case. Similar: Kebab.
v := str.Of("fooBar baz").Snake().String()
println(v)
// #string foo_bar_bazTitle 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 returns a lowercase copy of the string using Unicode rules. Similar: ToUpper and LcFirst.
v := str.Of("GoLang").ToLower().String()
println(v)
// #string golangToUpper returns an uppercase copy of the string using Unicode rules. Similar: ToLower and UcFirst.
v := str.Of("GoLang").ToUpper().String()
println(v)
// #string GOLANGUcFirst returns the string with the first rune upper-cased. Similar: LcFirst and ToUpper.
v := str.Of("gopher").UcFirst().String()
println(v)
// #string GopherIsASCII reports whether the string consists solely of 7-bit ASCII runes.
v := str.Of("gopher").IsASCII()
println(v)
// #bool trueIsAlnum 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 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 reports whether the string contains only Unicode whitespace. Similar: IsEmpty.
v := str.Of(" \t\n")
println(v.IsBlank())
// #bool trueIsEmpty reports whether the string has zero length. Similar: IsBlank.
v := str.Of("").IsEmpty()
println(v)
// #bool trueIsNumeric reports whether the string contains at least one rune and every rune is a Unicode number.
v := str.Of("12345").IsNumeric()
println(v)
// #bool trueDeduplicate 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 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 removes surrounding whitespace and collapses internal whitespace to single spaces. Similar: Trim.
v := str.Of(" go forj ").NormalizeSpace().String()
println(v)
// #string go forjTrim removes leading and trailing Unicode whitespace. Similar: TrimLeft, TrimRight, and TrimChars.
v := str.Of(" GoForj ").Trim().String()
println(v)
// #string GoForjTrimChars removes leading and trailing runes contained in cutset. Similar: Trim.
v := str.Of("..GoForj!!").TrimChars(".!").String()
println(v)
// #string GoForjTrimLeft removes leading Unicode whitespace. Similar: Trim and TrimRight.
v := str.Of(" GoForj ").TrimLeft().String()
println(v)
// #string GoForj\u0020\u0020TrimRight removes trailing Unicode whitespace. Similar: Trim and TrimLeft.
v := str.Of(" GoForj ").TrimRight().String()
println(v)
// #string \u0020\u0020GoForjEqualFold reports whether the string matches other using Unicode simple case folding.
v := str.Of("gopher").EqualFold("GOPHER")
println(v)
// #bool trueAppend concatenates the provided parts to the end of the string. Similar: Prepend.
v := str.Of("Go").Append("Forj", "!").String()
println(v)
// #string GoForj!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 WorldOf wraps a raw string with fluent helpers.
v := str.Of("gopher")
println(v.String())
// #string gopherBool 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 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 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 trueFromBase64 decodes a standard Base64 string. Similar: ToBase64.
v, err := str.Of("Z29waGVy").FromBase64()
println(v.String(), err == nil)
// #string gopher
// #bool trueToBase64 encodes the string using standard Base64. Similar: FromBase64.
v := str.Of("gopher").ToBase64().String()
println(v)
// #string Z29waGVyGoString allows %#v formatting to print the raw string.
v := str.Of("go")
println(fmt.Sprintf("%#v", v))
// #string goString returns the underlying raw string value.
v := str.Of("go").String()
println(v)
// #string goRuneCount returns the number of Unicode code points in the string.
v := str.Of("gophers 🦫").RuneCount()
println(v)
// #int 9Mask 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 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 truePadBoth 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 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 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...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 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 personRemove deletes all occurrences of provided substrings.
v := str.Of("The Go Toolkit").Remove("Go ").String()
println(v)
// #string The ToolkitReplaceAll 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 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 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 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 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 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 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 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!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 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 returns the number of non-overlapping occurrences of sub.
v := str.Of("gogophergo").Count("go")
println(v)
// #int 3HasPrefix 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 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 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 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 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 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 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-toolkitExcerpt 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...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 splits the string by the given separator.
v := str.Of("a,b,c").Split(",")
fmt.Println(v)
// #[]string [a b c]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 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 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 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 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 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 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 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 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 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 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 returns the first length runes of the string (clamped). Similar: TakeLast and Limit.
v := str.Of("gophers").Take(3).String()
println(v)
// #string gopTakeLast returns the last length runes of the string (clamped). Similar: Take.
v := str.Of("gophers").TakeLast(4).String()
println(v)
// #string hersRepeat repeats the string count times (non-negative).
v := str.Of("go").Repeat(3).String()
println(v)
// #string gogogoReverse returns a rune-safe reversed string.
v := str.Of("naïve").Reverse().String()
println(v)
// #string evïanFirstWord returns the first detected word or an empty string. Similar: LastWord and SplitWords.
v := str.Of("Hello world")
println(v.FirstWord().String())
// #string HelloInitials 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 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 returns the last detected word or an empty string. Similar: FirstWord and SplitWords.
v := str.Of("Hello world").LastWord().String()
println(v)
// #string worldSplitWords 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 returns the number of detected words. Similar: SplitWords.
v := str.Of("Hello, world!").WordCount()
println(v)
// #int 2Words 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 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.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.
