Help us shape the future of Go! Take the 2026 Go Developer Survey

gospell

package module
v0.9.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 20, 2026 License: MIT Imports: 16 Imported by: 18

README

gospell

Go Reference Build Status license

gospell is a pure Go spell checker for Hunspell-style dictionaries.

It is designed for:

  • loading Hunspell .aff and .dic files
  • exact word checking
  • word splitting and compound handling
  • per-document or per-session word list overlays
  • lazy mutation-based spelling suggestions

The package keeps the core checker small and uses lazy lookup for both spelling and suggestions.

Features

  • Pure Go implementation
  • Hunspell dictionary format support
  • Exact spell checking with affix and compound handling
  • WordList overlays — add allowed or forbidden words without touching the base dictionary
  • Checker — combines a base dictionary with any number of WordLists; supports per-document reset at zero cost
  • Built-in English/QWERTY mutation suggester with n-gram fallback

Install

go get github.com/client9/gospell

Basic Use

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

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)
}
Suggestion Engines

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:

  • query-time mutation generation
  • n-gram fallback scoring
  • chained or composite engines
Built-In Suggesters

gospell currently ships with one built-in suggestion engine:

  • NewMutationSuggester - generates English/QWERTY candidates on demand and falls back to n-gram root matching

Defaults:

  • MutationOptions{CandidateCap: 256, NGramRootCap: 64}
Mutation-Based Suggestions

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:

  • deletions and adjacent transpositions are always generated
  • substitutions prefer nearby keyboard keys first, then fall back to the English alphabet
  • insertions use nearby keyboard keys first, then the English alphabet

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.

Multiple Dictionaries

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.

Word Lists

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.

API Overview

The main entry points are:

Hunspell Compatibility

This package understands the Hunspell dictionary format and supports:

  • affix expansion
  • compound rules
  • iconv conversions
  • case handling
  • custom word lists

For a feature checklist and compatibility matrix, see docs/hunspell-compatibility.md.

Dictionary Files

The repository includes an English Hunspell dictionary used by tests and benchmarks:

  • hunspell-en_US/en_US.aff
  • hunspell-en_US/en_US.dic

Contributing

This project is still evolving. If you find a bug, a mismatch with Hunspell behavior, or a better suggestion strategy, patches are welcome.

License

MIT

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func AddDic added in v0.9.2

func AddDic(gs *GoSpell, name string, paths []string) error

AddDic finds a .dic file by searching paths and merges it into gs using AddDictionaryFile. Affix rules from gs are reused, so inflected forms (e.g. "colors" from "color/S") are recognized. The .dic file must be UTF-8.

func SearchPaths

func SearchPaths() []string

SearchPaths returns the ordered list of directories to search for named dictionaries. DICPATH (colon-separated, hunspell convention) is read first; standard system locations are appended as fallback. Callers may prepend their own tool-specific paths before passing the result to Open or OpenSupplement.

Types

type Checker

type Checker struct {
	// contains filtered or unexported fields
}

Checker combines a base GoSpell dictionary with zero or more WordLists. WordLists are consulted before the base dictionary: the first forbidden match rejects the word; the first allowed match accepts it; otherwise the base dictionary decides.

func NewChecker

func NewChecker(base *GoSpell) *Checker

NewChecker creates a Checker backed by base.

func (*Checker) AddWordList

func (c *Checker) AddWordList(wl *WordList)

AddWordList appends wl to the active word lists.

func (*Checker) InputConversion

func (c *Checker) InputConversion(raw []byte) string

InputConversion applies the base dictionary's ICONV rules to raw input.

func (*Checker) RemoveWordList

func (c *Checker) RemoveWordList(wl *WordList)

RemoveWordList removes wl from the active word lists by pointer identity.

func (*Checker) Spell

func (c *Checker) Spell(word string) bool

Spell reports whether word is correctly spelled.

func (*Checker) Suggest

func (c *Checker) Suggest(word string, limit int) ([]Suggestion, error)

Suggest returns spelling suggestions. The base dictionary suggester provides the primary results; active WordLists are scanned with brute-force edit distance and merged into the final set.

Known limitation: words in active WordLists are not indexed by the base suggester. Only the brute-force scan covers them, which is fast given typical WordList sizes.

type GoSpell

type GoSpell struct {
	// contains filtered or unexported fields
}

GoSpell is main struct

func NewGoSpell

func NewGoSpell(affFile, dicFile string) (*GoSpell, error)

NewGoSpell creates a GoSpell from Hunspell AFF and DIC file paths.

func NewGoSpellReader

func NewGoSpellReader(aff, dic io.Reader) (*GoSpell, error)

NewGoSpellReader creates a GoSpell from io.Readers for Hunspell AFF and DIC data.

func Open

func Open(name string, paths []string) (*GoSpell, error)

Open finds and loads a named base dictionary (.aff + .dic pair) by searching paths in order. name is a bare dictionary name ("en_US"), an absolute path, or a relative path containing a separator. The ".aff" or ".dic" extension is stripped if present. Returns a descriptive error if no matching pair is found in any of the provided paths.

func (*GoSpell) AddDictionaryFile added in v0.9.2

func (g *GoSpell) AddDictionaryFile(dicFile string) error

AddDictionaryFile opens dicFile and calls AddDictionaryReader.

func (*GoSpell) AddDictionaryReader added in v0.9.2

func (g *GoSpell) AddDictionaryReader(dic io.Reader) error

AddDictionaryReader merges an additional Hunspell .dic file into this GoSpell, applying the same affix rules as the base dictionary. The file must be UTF-8 encoded. The word-count header line is optional: if the first line cannot be parsed as an integer it is treated as a word entry.

Intended for load-time use; not safe for concurrent calls with Spell or Suggest.

func (*GoSpell) AddWordRaw

func (s *GoSpell) AddWordRaw(word string) bool

AddWordRaw adds a single word to the internal dictionary without modifications. Returns true if added, false if already present.

func (*GoSpell) ForEachWord

func (s *GoSpell) ForEachWord(fn func(word string) bool)

ForEachWord calls fn for every word in the dictionary. Iteration stops early if fn returns false.

func (*GoSpell) HasWord

func (s *GoSpell) HasWord(word string) bool

HasWord reports whether word exists in the dictionary as an exact entry.

func (*GoSpell) InputConversion

func (s *GoSpell) InputConversion(raw []byte) string

InputConversion does any character substitution before checking based on the ICONV stanza in the AFF file.

func (*GoSpell) MaxWordLen

func (s *GoSpell) MaxWordLen() int

MaxWordLen returns the longest loaded dictionary word length.

func (*GoSpell) SetSuggester

func (s *GoSpell) SetSuggester(suggester Suggestions) error

SetSuggester configures the suggestion engine and initializes it from the current dictionary contents.

func (*GoSpell) Spell

func (s *GoSpell) Spell(word string) bool

Spell reports whether word is correctly spelled.

func (*GoSpell) Suggest

func (s *GoSpell) Suggest(word string, limit int) ([]Suggestion, error)

Suggest returns spelling suggestions using the configured suggester.

func (*GoSpell) WordCount

func (s *GoSpell) WordCount() int

WordCount returns the number of dictionary entries currently loaded.

type MutationOptions

type MutationOptions struct {
	CandidateCap int
	NGramRootCap int
}

MutationOptions controls the query-time mutation suggester.

type MutationSuggester

type MutationSuggester struct {
	// contains filtered or unexported fields
}

MutationSuggester generates one-edit candidates from the query word and checks each candidate directly against the dictionary. It does not build an inverted index up front.

func NewMutationSuggester

func NewMutationSuggester(opts MutationOptions) *MutationSuggester

NewMutationSuggester creates a mutation-based suggester.

func (*MutationSuggester) Init

Init stores the suggestion source for later membership checks.

func (*MutationSuggester) Suggest

func (s *MutationSuggester) Suggest(word string, limit int) ([]Suggestion, error)

Suggest returns the best matches ordered by edit distance, then keyboard penalty, then lexicographic order.

type Suggestion

type Suggestion struct {
	Word  string
	Score int
}

Suggestion is a ranked candidate returned by a suggestion engine.

type SuggestionSource

type SuggestionSource interface {
	ForEachWord(func(word string) bool)
	HasWord(word string) bool
	WordCount() int
	MaxWordLen() int
}

SuggestionSource provides read-only access to a loaded dictionary so a suggester can build indexes or other internal structures.

type Suggestions

type Suggestions interface {
	Init(src SuggestionSource) error
	Suggest(word string, limit int) ([]Suggestion, error)
}

Suggestions is the pluggable suggestion engine interface used by GoSpell.

type WordList

type WordList struct {
	// contains filtered or unexported fields
}

WordList is a small overlay of allowed and forbidden words used as a per-context supplement to a base GoSpell dictionary. Multiple WordLists can be active simultaneously; see Checker. Loading policy — global or per-document — is the caller's responsibility.

Format (one entry per line):

# comment
word          → allowed
*word         → forbidden

func NewWordList

func NewWordList(r io.Reader) (*WordList, error)

NewWordList parses a WordList from r.

func NewWordListFile

func NewWordListFile(name string) (*WordList, error)

NewWordListFile parses a WordList from a file.

func NewWordListFromDic

func NewWordListFromDic(r io.Reader) (*WordList, error)

NewWordListFromDic parses a Hunspell .dic file from r and returns the word stems as an allowed WordList. The word-count header line is skipped; affix flags (the part after '/') are stripped from each entry.

This is distinct from NewWordList, which parses the personal word list format (plain words or *word for forbidden entries). Use NewWordListFromDic when loading a raw Hunspell .dic file as a vocabulary supplement.

func OpenSupplement

func OpenSupplement(name string, paths []string) (*WordList, error)

OpenSupplement finds and loads a .dic-only domain supplement (no .aff file) by searching paths in order. The result is a WordList ready to attach to a Checker. Supplement dictionaries extend the vocabulary of a base dictionary with domain-specific terms (medical, legal, technical) without affix processing. Encoding is assumed UTF-8; for non-UTF-8 supplements use a full base dictionary (.aff + .dic) as the first entry instead.

func (*WordList) Add

func (wl *WordList) Add(word string)

Add adds word to the allowed set.

func (*WordList) Forbid

func (wl *WordList) Forbid(word string)

Forbid adds word to the forbidden set.

func (*WordList) HasWord

func (wl *WordList) HasWord(word string) bool

HasWord reports whether word is in the allowed set.

func (*WordList) IsForbidden

func (wl *WordList) IsForbidden(word string) bool

IsForbidden reports whether word is in the forbidden set.

Directories

Path Synopsis
cmd
gospell command

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL