htmlquery: XPath over HTML in Go, with a cache you can switch off
htmlquery is golang XPath package for HTML query.
At a glance
- What is it?
- htmlquery is a Go package that runs XPath 1.0 and 2.0 expressions against HTML documents, with an LRU cache of compiled expressions and a global switch to turn it off. It is a thin layer over the antchfx/xpath engine, useful for scrapers that want XPath semantics without writing a browser.
- Who is it for?
- Adopt htmlquery if you are writing a Go scraper that wants XPath over real HTML rather than a forgiving CSS selector engine, and if you want one API shape shared with xmlquery and jsonquery. Do not adopt it if you need a headless browser, JavaScript execution or a network client with retries and rate limiting, because fetching is a thin wrapper over the HTTP request and nothing here renders or waits.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 4 days ago.
- What is it written in?
- Mainly Go, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
XPath 1.0 and 2.0 expressions against a parsed HTML tree
htmlquery exists to answer one question: given a chunk of HTML, how do I pull out the nodes I want using XPath. The package description is blunt about it, calling htmlquery an XPath query package for HTML that lets you extract data or evaluate from HTML documents by an XPath expression. The expression syntax is documented on a separate page, the antchfx/xpath repository, and the README links there for the supported XPath 1.0 and 2.0 syntax. That separation is the key architectural fact. htmlquery does not implement an XPath engine of its own. The go.mod file shows the dependency:
module github.com/antchfx/htmlquery
go 1.14
require (
github.com/antchfx/xpath v1.3.9
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da
golang.org/x/net v0.33.0
)So the expression compiler, the function library and the evaluation rules live in antchfx/xpath, and htmlquery supplies the HTML side: parsing documents and exposing matched nodes. The repository layout is correspondingly thin, with cache.go and query.go at the top level next to the tests. For a Go engineer, the appeal is that XPath gives you a query language with axes, predicates and functions, which means you can express a selection that would take several CSS steps, and that the same expression syntax works across documents.
Four ways to load a document: URL, file, string, or an existing tree
The library separates fetching from querying, and the README shows four entry points for getting a document into the query engine. Loading from a URL:
doc, err := htmlquery.LoadURL("http://example.com/")Loading from a file on disk:
filePath := "/home/user/sample.html"
doc, err := htmlquery.LoadDoc(filePath)Parsing a string you already hold:
s := `<html>....</html>`
doc, err := htmlquery.Parse(strings.NewReader(s))Or starting from a document you obtained yourself. The fourth path matters most in practice, because the HTTP fetch is deliberately minimal. The go.mod pulls in golang.org/x/net v0.33.0, which is where the HTML parsing comes from, and the only HTTP handling visible in the source files is query_http_test.go, a test file. That tells you the fetch helper exists but that network policy is not the package's job. There is no mention of retries, timeouts, proxy settings or rate limiting in the README, so if you point LoadURL at a site you do not control, you are responsible for being a polite client. For anything with more than a handful of pages, the sensible shape is to fetch with a client you control and hand the bytes to htmlquery.Parse.
QueryAll returns an error where Find panics
The README answers the difference between the two main query functions in its FAQ section, and it is a real one rather than a naming preference. Both search for all matching HTML nodes. Find will panic if you give it an invalid XPath expression, while QueryAll returns the error to you. The example in the README shows the error-returning form:
nodes, err := htmlquery.QueryAll(doc, "//a")
if err != nil {
panic(`not a valid XPath expression.`)
}The Find form is shorter, which is why it appears in most of the examples:
list := htmlquery.Find(doc, "//a")The choice depends on where the expression comes from. If you hardcode the expression, a panic on a typo surfaces immediately during development and costs nothing. If the expression arrives from a configuration file, a user-supplied filter or an HTTP parameter, then QueryAll is the only correct choice, because a malformed expression from outside your code should be an error value and not a crash. The README also notes that the Find and QueryAll pair is not the whole surface: there are QuerySelector and QuerySelectorAll methods that accept an already compiled expression object, and reusing a compiled expression avoids recompiling the XPath on every call. Below them sit the convenience selectors, FindOne for a single match, InnerText to read an element's text, and SelectAttr to pull an attribute value such as href or src.
The LRU cache stores compiled expressions and can be disabled globally
The most interesting mechanism in the package is the query cache, implemented in cache.go on top of the LRU implementation from github.com/golang/groupcache/lru. The README explains the purpose plainly: it caches recently used XPath query strings so that a repeated expression is not recompiled on every query. Caching is on by default, and the README publishes a benchmark comparing the two paths on windows and amd64:
BenchmarkSelectorCache-4 20000000 55.2 ns/op
BenchmarkDisableSelectorCache-4 500000 3162 ns/opRead that as a benchmark for this project on one machine rather than a guarantee for your workload, but the direction is what you would expect from avoiding repeated parsing and compilation of the same expression. The toggle is a package-level variable, set as follows:
htmlquery.DisableSelectorCache = trueThat is a global switch, not a per-call option, which makes it a decision you take once at process start rather than a knob you can turn per query. Two practical consequences follow. In a long-running service where a small set of expressions dominates, leaving the cache on is the right default. In a one-shot command line tool that queries once and exits, the cache costs a map and a little memory with no benefit, and turning it off removes work rather than adding it. The switch also matters when you are measuring: a benchmark that silently uses the cached path tells you something different from one that does not.
Counting and evaluating needs the xpath package directly
Some queries are not about returning nodes at all. The README shows how to evaluate an expression numerically, such as counting elements, and it does this by reaching for the xpath package explicitly:
expr, _ := xpath.Compile("count(//img)")
v := expr.Evaluate(htmlquery.CreateXPathNavigator(doc)).(float64)
fmt.Printf("total count is %f", v)The mechanism is CreateXPathNavigator, which wraps the HTML document in an interface the xpath engine can walk. That function is the seam between the two packages, and it is also the route to the full function library and the XPath 2.0 features. Note the pattern: compile the expression, evaluate it against the navigator, and type-assert the result, in this case to float64. The type assertion is unchecked, so an expression whose result is not a float64 will panic rather than return a zero value. This is also the honest way to reach features the htmlquery helpers do not wrap. If you need node-set manipulations, string functions beyond InnerText, or anything the sugar does not expose, drop to antchfx/xpath and drive it yourself.
One query shape across HTML, XML and JSON, but no rendering
The antchfx family gives this package its clearest alternative. The README lists three siblings: htmlquery for HTML documents, xmlquery for XML documents, and jsonquery for JSON documents, all XPath query packages. The difference in approach is a uniform query surface over three document models, so a team that works with all three formats learns one expression language and one API shape. That consistency is the strongest argument for htmlquery over a format-specific parser. The limit is equally clear. htmlquery is a query layer over already-parsed text. It does not execute JavaScript, it does not run a layout engine, and it does not wait for a client-side application to render. For a page whose content arrives through a script, the query will run against a document that does not contain the data yet, and no amount of XPath tuning fixes that. The second gap is networking, mentioned above: the URL loader is a convenience, not a crawler. If your requirement involves rendering, session state, rate limits or proxy rotation, you need a browser automation tool or an HTTP client with those features, and htmlquery remains useful as the extraction layer underneath them.
Releases are frequent and the module floor is old
The project is actively changing. The last push was on 2026-10-19, and the recent release tags are v1.3.7 on 2026-10-05, v1.3.6 on 2026-03-06 and v1.3.5 on 2025-11-10. The 1.3.x line has been stable across a year of tags, which is the pattern you want from a library whose API surface most teams build on. The module declaration, however, says `go 1.14`, a floor from 2020. That is a compatibility promise rather than a limitation, and it means the package builds on old toolchains, but it also tells you the module metadata is not aggressively modernised. Licensing is MIT, so you can use htmlquery in commercial and closed source code, keeping the license and copyright notice in your distribution. The practical upgrade cost is low: a patch-level bump inside 1.3.x should not move the API, and the only companion dependency you need to track is the antchfx/xpath version your build resolves, since expression semantics ultimately come from there.
Editorial conclusion
Adopt htmlquery if you are writing a Go scraper that wants XPath over real HTML rather than a forgiving CSS selector engine, and if you want one API shape shared with xmlquery and jsonquery. Do not adopt it if you need a headless browser, JavaScript execution or a network client with retries and rate limiting, because fetching is a thin wrapper over the HTTP request and nothing here renders or waits. Before committing, decide whether you want the compiled-expression cache enabled, since htmlquery.DisableSelectorCache is a process-wide variable and the README's own benchmark puts the cached path at 55.2 ns/op against 3162 ns/op for the uncached path, then confirm the antchfx/xpath version your build resolves against github.com/antchfx/xpath v1.3.9.
Frequently asked questions
How do I install htmlquery in a Go project?
The README gives a single command: `go get github.com/antchfx/htmlquery`. The module path is github.com/antchfx/htmlquery and the go.mod declares `go 1.14`, so it builds on old toolchains as well as current ones.
What is the difference between htmlquery.Find and htmlquery.QueryAll?
Both search for all matching HTML nodes. Find panics when the XPath expression is invalid, while QueryAll returns the error, so QueryAll is the safer choice for expressions that come from outside your code.
How do I turn off the XPath query cache in htmlquery?
Set the package variable `htmlquery.DisableSelectorCache = true`. The cache is enabled by default and stores recently used XPath query strings so they are not recompiled on every query.
Can htmlquery handle pages that render with JavaScript?
No. htmlquery parses and queries HTML text, and the README documents no rendering or script execution, so content loaded client side is not present in the document it queries. Use a browser automation layer and pass the resulting HTML to htmlquery.Parse if you need that.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/antchfx-htmlquery)Was this useful?