chore: break name package dep on root package#82
Open
stanislas-m wants to merge 4 commits into
Open
Conversation
- Introduce an internal package to share core functions - Avoid any dep from inner packages to outer / root packages - Fix performance / allocation issues (validated with benchmarks)
1061120 to
e0f91ec
Compare
paganotoni
requested changes
Jul 1, 2026
paganotoni
left a comment
Member
There was a problem hiding this comment.
This is great @stanislas-m, thanks for putting the time into modernizing flect. A couple of suggestions:
-
Could we reduce the number of files at the root and simply have a flect.go file in there.
-
instead of naming the internal package "core" could we call if flect or simply have the core files live at the root of the internal folder and be part of the flect/internal package?
Member
Author
|
Hey @paganotoni, it's been a while. ;)
|
We only need to check for existance of the key, map[string]struct{}
takes no space to store the value.
Member
Author
|
Should be good now:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Performance optimisations & internal refactor
Context
The original package had no internal structure — all logic lived flat in the root package and depended on
testifyfor tests. This PR does two things:Structural refactor: moves all implementation into
internal/core, leaving the root andnamepackages as thin public wrappers. A sharedinternal/testhelperspackage replaces thetestifydependency, making the module dependency-free.Performance pass: seven targeted fixes to
internal/coreidentified by profiling and allocation analysis.Performance fixes
1.
isSpace:slices.Contains→switch(core.go)The hot inner loop in
toPartscalledslices.Containsover a 5-element slice on every character. Replaced with aswitchstatement that compiles to a jump table. Also removed the now-unusedspacesvar andslicesimport.2.
xappend: singleTrimFuncpass instead of loop (core.go)The five-iteration trim loop called
strings.Trim(s, string(x))per space character — five allocations perxappendcall. Replaced with onestrings.TrimFunc(s, isSpace)pass.3.
WriteString(string(rune))→WriteRune(camelize.go,dasherize.go)Four sites were converting a
runetostringbefore passing it tostrings.Builder.WriteString, allocating a tiny string per character. Replaced withWriteRune.4. Redundant
strings.ToUppercalls intoPartsandxappend(ident.go,core.go)strings.ToUpper(s)twice; now computed once.BaseAcronyms[strings.ToUpper(x.String())]) was provably redundant: when that branch fires, both the current and previous rune are uppercase, soxis always all-uppercase. Thestrings.ToUppercall is dropped entirely.xappend's acronym check also calledstrings.ToUppertwice; now computed once.5. O(n²) rule building at init → O(n) (
plural_rules.go)InsertPluralRuleandInsertSingularRuleprepend by allocating a new backing array each call. With ~150 init-time insertions this was O(n²). The secondinit()now buildspluralRulesandsingularRulesdirectly in one pre-allocated forward pass, replicating the same priority order.InsertPluralRule/InsertSingularRuleare left unchanged for runtime user customisation.6. Mutex held across O(n) rule scan → released before scan (
pluralize.go,singularize.go)defer pluralMoot.RUnlock()held the lock for the entire function body, including the linear scan of 200+ rules. Replaced with explicit per-return-site unlocks. The rules slice header is captured under the lock before release — safe becauseInsertPluralRulealways creates a new backing array rather than mutating in place.7.
New()re-parse on transform output eliminated (9 files)Every transform method ended with
return New(result), runningtoPartson the output string unnecessarily. For transforms whose result is never chained into another method that reads.Parts(Camelize,Pascalize,Titleize,Humanize,Dasherize,Capitalize,Ordinalize,ToUpper,ToLower), this is replaced withreturn Ident{Original: result}.Underscore,Pluralize, andSingularizeretainNew()because they are chained innamepackage chains that access.Partsdownstream.Bonus: the
x.String() != ""empty check inCamelizeis replaced withx.Len() > 0(saves one allocation per part).8.
fmt.Sprintf→strconv.ItoainOrdinalize(ordinalize.go)Four
fmt.Sprintf("%d<suffix>", number)calls replaced with a singlestrconv.Itoa(number) + suffixafter computing the suffix in a switch. Thefmtimport is removed entirely.Benchmark results
All benchmarks run with
-benchmem -benchtime=3son AMD Ryzen 9 3900X. Each benchmark exercises 12 representative inputs covering plain words, snake_case, CamelCase, acronyms, and multi-segment paths.CamelizePascalizeDasherizeTitleizeHumanizeCapitalizeOrdinalizeNewUnderscorePluralize*Singularize**
Pluralize/Singularizeiterate the full inflection table (~200 words), not the 12-input set, so they are not directly comparable to the others. Their modest gain comes from the mutex narrowing fix; the bottleneck is the O(n) rule scan itself.Pascalizeis the biggest winner (2.66×, −69% allocs) because it chains throughCamelizeinternally — before this PR it paidtoPartsthree times per call (input parse, Camelize output re-parse, Pascalize output re-parse) and now pays it once.Other changes
SHOULDERS.mdremoved: listed six packages that were all transitivetestifydependencies; the module now has zero external dependencies (go.sumis empty).bench_test.goadded: benchmarks withb.ReportAllocs()for all transforms that previously had none.