gospell is a pure Go spell checker for Hunspell-style dictionaries.
It is designed for:
.aff and .dic filesThe package keeps the core checker small and uses lazy lookup for both spelling and suggestions.
WordList overlays — add allowed or forbidden words without touching the base dictionaryChecker — combines a base dictionary with any number of WordLists; supports per-document reset at zero costgo get github.com/client9/gospell
package main
import (
"fmt"
"log"
"github.com/client9/gospell"
)
func main() {
gs, err := gospell.NewGoSpell("hunspell-en_US/en_US.aff", "hunspell-en_US/en_US.dic")
if err != nil {
log.Fatal(err)
}
fmt.Println(gs.Spell("silly"))
fmt.Println(gs.Spell("sillly"))
}
Suggestions use the built-in mutation suggester by default. The suggester generates common typo mutations first, then falls back to an n-gram root scan that expands only the best matching roots.
gs, err := gospell.NewGoSpell("hunspell-en_US/en_US.aff", "hunspell-en_US/en_US.dic")
if err != nil {
log.Fatal(err)
}
sugs, err := gs.Suggest("sillly", 5)
if err != nil {
log.Fatal(err)
}
for _, sug := range sugs {
fmt.Println(sug.Word, sug.Score)
}
You can still replace the default suggester if you provide your own engine:
if err := gs.SetSuggester(gospell.NewMutationSuggester(
gospell.MutationOptions{CandidateCap: 256},
)); err != nil {
log.Fatal(err)
}
gospell ships with a small interface so you can swap suggestion strategies without changing the checker:
type Suggestions interface {
Init(src SuggestionSource) error
Suggest(word string, limit int) ([]Suggestion, error)
}
This makes it easy to experiment with:
gospell currently ships with one built-in suggestion engine:
NewMutationSuggester - generates English/QWERTY candidates on demand and falls back to n-gram root matchingDefaults:
MutationOptions{CandidateCap: 256, NGramRootCap: 64}The mutation suggester takes the misspelled word, generates a bounded set of
single-edit candidates, and checks each candidate with the dictionary's lazy
Spell method. If no mutation survives, it scans root dictionary entries by
n-gram similarity, expands only the best roots, and reranks those candidates.
It does not materialize all dictionary surfaces.
The current first pass assumes English and a QWERTY keyboard:
That keeps startup cost near zero while still finding common typos such as missing letters, doubled letters, transpositions, and near-key substitutions.
See docs/mutation-suggestions.md
for a longer explanation of the approach and tradeoffs.
A common pattern is to combine a base dictionary with domain-specific vocabulary (medical, legal, technical) that shares the same affix rules. AddDictionaryReader and AddDictionaryFile merge an additional .dic file into an existing GoSpell, applying the same affix expansion as the base — so entries like widget/S produce both "widget" and "widgets".
This is the key difference from OpenSupplement/NewWordListFromDic, which strip affix flags and only recognize bare stems.
gs, err := gospell.NewGoSpell("en_US.aff", "en_US.dic")
if err != nil {
log.Fatal(err)
}
// Merge a domain-specific dictionary reusing the en_US affix rules.
if err := gs.AddDictionaryFile("medical.dic"); err != nil {
log.Fatal(err)
}
// Or use AddDic for path-based search:
if err := gospell.AddDic(gs, "medical", searchPaths); err != nil {
log.Fatal(err)
}
These are load-time operations; call them before using Spell or Suggest from multiple goroutines.
WordList is a lightweight overlay of allowed and forbidden words. It does not require rebuilding the base dictionary and can be attached or detached at any time.
Format: one entry per line. Lines starting with # are comments. Lines starting with * forbid the word. All other non-blank lines allow the word.
# project-specific terms
Kubernetes
gRPC
*irregardless
gs, err := gospell.NewGoSpell("en_US.aff", "en_US.dic")
if err != nil {
log.Fatal(err)
}
checker := gospell.NewChecker(gs)
// Load a global personal word list once.
global, err := gospell.NewWordListFile("personal.txt")
if err != nil {
log.Fatal(err)
}
checker.AddWordList(global)
// Per-document: add, use, then remove.
doc, _ := gospell.NewWordList(strings.NewReader("ProjectName\n*badterm\n"))
checker.AddWordList(doc)
fmt.Println(checker.Spell("ProjectName")) // true
fmt.Println(checker.Spell("badterm")) // false
checker.RemoveWordList(doc) // reset for next document
Checker.Suggest merges base dictionary suggestions with a brute-force scan of all active WordLists, so per-document words appear in suggestions automatically.
The main entry points are:
NewGoSpell / NewGoSpellReader — load a base dictionary(*GoSpell).AddDictionaryFile / AddDictionaryReader — merge additional .dic files with full affix expansionAddDic — path-search wrapper for AddDictionaryFileNewChecker — runtime query API wrapping a base dictionary(*Checker).Spell — spell check against base + all active WordLists(*Checker).Suggest — suggestions from base + WordLists(*Checker).AddWordList / RemoveWordListNewWordList / NewWordListFile(*GoSpell).Spell — direct base-only check (no WordLists)(*GoSpell).SuggestNewMutationSuggesterThis package understands the Hunspell dictionary format and supports:
For a feature checklist and compatibility matrix, see
docs/hunspell-compatibility.md.
The repository includes an English Hunspell dictionary used by tests and benchmarks:
hunspell-en_US/en_US.affhunspell-en_US/en_US.dicThis project is still evolving. If you find a bug, a mismatch with Hunspell behavior, or a better suggestion strategy, patches are welcome.
106 commits
6 commits
Go
99.4%
gospell is a pure Go spell checker for Hunspell-style dictionaries.
It is designed for:
.aff and .dic filesThe package keeps the core checker small and uses lazy lookup for both spelling and suggestions.
WordList overlays — add allowed or forbidden words without touching the base dictionaryChecker — combines a base dictionary with any number of WordLists; supports per-document reset at zero costgo get github.com/client9/gospell
package main
import (
"fmt"
"log"
"github.com/client9/gospell"
)
func main() {
gs, err := gospell.NewGoSpell("hunspell-en_US/en_US.aff", "hunspell-en_US/en_US.dic")
if err != nil {
log.Fatal(err)
}
fmt.Println(gs.Spell("silly"))
fmt.Println(gs.Spell("sillly"))
}
Suggestions use the built-in mutation suggester by default. The suggester generates common typo mutations first, then falls back to an n-gram root scan that expands only the best matching roots.
gs, err := gospell.NewGoSpell("hunspell-en_US/en_US.aff", "hunspell-en_US/en_US.dic")
if err != nil {
log.Fatal(err)
}
sugs, err := gs.Suggest("sillly", 5)
if err != nil {
log.Fatal(err)
}
for _, sug := range sugs {
fmt.Println(sug.Word, sug.Score)
}
You can still replace the default suggester if you provide your own engine:
if err := gs.SetSuggester(gospell.NewMutationSuggester(
gospell.MutationOptions{CandidateCap: 256},
)); err != nil {
log.Fatal(err)
}
gospell ships with a small interface so you can swap suggestion strategies without changing the checker:
type Suggestions interface {
Init(src SuggestionSource) error
Suggest(word string, limit int) ([]Suggestion, error)
}
This makes it easy to experiment with:
gospell currently ships with one built-in suggestion engine:
NewMutationSuggester - generates English/QWERTY candidates on demand and falls back to n-gram root matchingDefaults:
MutationOptions{CandidateCap: 256, NGramRootCap: 64}The mutation suggester takes the misspelled word, generates a bounded set of
single-edit candidates, and checks each candidate with the dictionary's lazy
Spell method. If no mutation survives, it scans root dictionary entries by
n-gram similarity, expands only the best roots, and reranks those candidates.
It does not materialize all dictionary surfaces.
The current first pass assumes English and a QWERTY keyboard:
That keeps startup cost near zero while still finding common typos such as missing letters, doubled letters, transpositions, and near-key substitutions.
See docs/mutation-suggestions.md
for a longer explanation of the approach and tradeoffs.
A common pattern is to combine a base dictionary with domain-specific vocabulary (medical, legal, technical) that shares the same affix rules. AddDictionaryReader and AddDictionaryFile merge an additional .dic file into an existing GoSpell, applying the same affix expansion as the base — so entries like widget/S produce both "widget" and "widgets".
This is the key difference from OpenSupplement/NewWordListFromDic, which strip affix flags and only recognize bare stems.
gs, err := gospell.NewGoSpell("en_US.aff", "en_US.dic")
if err != nil {
log.Fatal(err)
}
// Merge a domain-specific dictionary reusing the en_US affix rules.
if err := gs.AddDictionaryFile("medical.dic"); err != nil {
log.Fatal(err)
}
// Or use AddDic for path-based search:
if err := gospell.AddDic(gs, "medical", searchPaths); err != nil {
log.Fatal(err)
}
These are load-time operations; call them before using Spell or Suggest from multiple goroutines.
WordList is a lightweight overlay of allowed and forbidden words. It does not require rebuilding the base dictionary and can be attached or detached at any time.
Format: one entry per line. Lines starting with # are comments. Lines starting with * forbid the word. All other non-blank lines allow the word.
# project-specific terms
Kubernetes
gRPC
*irregardless
gs, err := gospell.NewGoSpell("en_US.aff", "en_US.dic")
if err != nil {
log.Fatal(err)
}
checker := gospell.NewChecker(gs)
// Load a global personal word list once.
global, err := gospell.NewWordListFile("personal.txt")
if err != nil {
log.Fatal(err)
}
checker.AddWordList(global)
// Per-document: add, use, then remove.
doc, _ := gospell.NewWordList(strings.NewReader("ProjectName\n*badterm\n"))
checker.AddWordList(doc)
fmt.Println(checker.Spell("ProjectName")) // true
fmt.Println(checker.Spell("badterm")) // false
checker.RemoveWordList(doc) // reset for next document
Checker.Suggest merges base dictionary suggestions with a brute-force scan of all active WordLists, so per-document words appear in suggestions automatically.
The main entry points are:
NewGoSpell / NewGoSpellReader — load a base dictionary(*GoSpell).AddDictionaryFile / AddDictionaryReader — merge additional .dic files with full affix expansionAddDic — path-search wrapper for AddDictionaryFileNewChecker — runtime query API wrapping a base dictionary(*Checker).Spell — spell check against base + all active WordLists(*Checker).Suggest — suggestions from base + WordLists(*Checker).AddWordList / RemoveWordListNewWordList / NewWordListFile(*GoSpell).Spell — direct base-only check (no WordLists)(*GoSpell).SuggestNewMutationSuggesterThis package understands the Hunspell dictionary format and supports:
For a feature checklist and compatibility matrix, see
docs/hunspell-compatibility.md.
The repository includes an English Hunspell dictionary used by tests and benchmarks:
hunspell-en_US/en_US.affhunspell-en_US/en_US.dicThis project is still evolving. If you find a bug, a mismatch with Hunspell behavior, or a better suggestion strategy, patches are welcome.
106 commits
6 commits
Go
99.4%