GeoVault is a production-ready Go library for managing and querying MaxMind GeoIP2/GeoLite2 MMDB databases. It serves lookups straight from a locally cached database — no network round-trips — while a background updater keeps the data fresh.
- City & ASN lookups — full GeoLite2 City and ASN records, strongly typed
- IPv4 & IPv6 — plus
net.IPinputs and DNS resolution for hostnames - Zero network I/O during lookups — reads are pure local MMDB operations
- Automatic background updates — HTTP metadata-based change detection
- Atomic replacement — updates swap readers without interrupting lookups
- Concurrent-safe — lookups, updates, and
Close()race-tested with-race - SHA-256 integrity — hash verification as a final download check
- Minimal dependencies — standard library plus one MMDB reader
- Installation
- Quick start
- Lookups
- IP literals,
net.IP, and hostnames - Configuration
- Updates
- Update detection
- Thread safety
- Errors
- Logging
go get github.com/obeliskdev/geovaultGeoLite2 databases are provided by MaxMind under a free license. Using the default download URLs means you agree to MaxMind's GeoLite2 end-user license agreement.
package main
import (
"fmt"
"log"
"github.com/obeliskdev/geovault"
)
func main() {
geo, err := geovault.New()
if err != nil {
log.Fatal(err)
}
defer geo.Close()
result, err := geo.Lookup("8.8.8.8")
if err != nil {
log.Fatal(err)
}
fmt.Println(result.Country.Code) // US
fmt.Println(result.City.Name) // Mountain View
fmt.Println(result.Location.Latitude) // 37.386
fmt.Println(result.Location.Longitude)
fmt.Println(result.ASN) // 15169
fmt.Println(result.Organization) // Google LLC
}geovault.New() downloads missing databases on first use, loads any valid local
copies, and — when auto-update is enabled — refreshes them in the background.
result, err := geo.LookupCity("8.8.8.8")
result.IP // "8.8.8.8"
result.Country.Code // "US"
result.Country.Name // "United States"
result.Country.Names // map["en"]"United States", ...
result.Continent.Code // "NA"
result.City.Name // "Mountain View"
result.Subdivisions // []Subdivision (full hierarchy)
result.Postal.Code // "94043"
result.Location.Latitude // 37.386
result.Location.Longitude // -122.0838
result.Location.TimeZone // "America/Los_Angeles"
result.Location.AccuracyRadius
result.Location.MetroCode
result.RegisteredCountry // Country
result.RepresentedCountry // Country
result.Traits // MaxMind traitsName is a convenience field derived from Names (English preferred); Names
carries every localized value the database provides.
result, err := geo.LookupASN("8.8.8.8")
result.ASN // 15169
result.Organization // "Google LLC"
result.AutonomousSystemNumber // 15169
result.AutonomousSystemOrganization // "Google LLC"result, err := geo.Lookup("8.8.8.8")The unified result combines City and ASN data whenever both databases are available; otherwise the available half is returned.
geo.Lookup(addr) // LookupResult (City + ASN)
geo.LookupCity(addr) // CityResult
geo.LookupASN(addr) // ASNResult
geo.LookupCountry(addr) // Country
geo.Country(addr) // Country (alias)
geo.ASN(addr) // ASNResult (alias)Every method accepts an IP literal, and IP-suffixed variants accept a
net.IP:
geo.LookupIP(net.ParseIP("8.8.8.8"))
geo.LookupCityIP(net.ParseIP("8.8.8.8"))
geo.LookupASNIP(net.ParseIP("8.8.8.8"))
geo.LookupCountryIP(net.ParseIP("8.8.8.8"))
geo.CountryIP(net.ParseIP("8.8.8.8"))
geo.ASNIP(net.ParseIP("8.8.8.8"))When the string form receives a hostname, GeoVault resolves it via DNS before looking up, preferring an IPv4 address:
result, err := geo.Lookup("example.com") // result.IP is the resolved addressLookups by IP literal are pure local MMDB reads with no network I/O; only hostname inputs trigger a DNS query.
geo, err := geovault.New(
geovault.WithDataDir("./geoip"),
geovault.WithCityDatabaseURL("https://example.com/city.mmdb"),
geovault.WithASNDatabaseURL("https://example.com/asn.mmdb"),
geovault.WithAutoUpdate(true),
geovault.WithUpdateInterval(12*time.Hour),
geovault.WithCacheTTL(30*time.Minute),
)| Option | Description | Default |
|---|---|---|
WithDataDir(path) |
Directory for databases and metadata. | OS cache dir |
WithCityDatabaseURL(url) |
City database URL. | MaxMind GeoLite2 |
WithASNDatabaseURL(url) |
ASN database URL. | MaxMind GeoLite2 |
WithCityDatabaseFile(name) |
Local City filename. | GeoLite2-City.mmdb |
WithASNDatabaseFile(name) |
Local ASN filename. | GeoLite2-ASN.mmdb |
WithAutoUpdate(bool) |
Enable the background updater. | false |
WithUpdateInterval(d) |
Background update interval. | 24h |
WithCacheTTL(d) |
Cache duration for metadata checks. | 1h |
WithHTTPClient(c) |
Custom HTTP client for downloads. | default client |
WithTimeout(d) |
Timeout for the default HTTP client. | 30s |
WithUserAgent(s) |
User-Agent header. | geovault |
WithDownloadRetries(n) |
Download retry attempts. | 3 |
WithRetryDelay(d) |
Delay between retries. | 2s |
WithLogger(l) |
Logger implementing Logger. |
no-op |
WithOnUpdate(fn) |
Callback after a successful update. | — |
WithOnError(fn) |
Callback on background update failure. | — |
geo, err := geovault.New(
geovault.WithAutoUpdate(true),
geovault.WithUpdateInterval(24*time.Hour),
)The updater runs in its own goroutine and never blocks lookups. It ticks on the
update interval, stops cleanly on Close(), never overlaps updates, and keeps
the active database when an update fails.
err := geo.Update(ctx) // check both (respects cache TTL)
err = geo.UpdateCity(ctx) // check City only
err = geo.UpdateASN(ctx) // check ASN only
err = geo.ForceUpdate(ctx) // check both, bypassing the cache TTLManual updates use the same safe mechanism as the background updater.
geo, err := geovault.New(
geovault.WithCityDatabaseURL("https://cdn.example.com/city.mmdb"),
geovault.WithASNDatabaseURL("https://cdn.example.com/asn.mmdb"),
)Successful metadata checks are cached for WithCacheTTL. Repeated Update
calls within that window skip the remote check; use ForceUpdate to bypass it.
When checking for remote changes, GeoVault applies, in order:
- Last-Modified — the HTTP header, via conditional
If-Modified-Sincerequests and304 Not Modifiedhandling. - Content-Length — remote size vs. local size, used only when no stronger metadata is available.
- Download + SHA-256 — download to a temp file and compare hashes, discarding the download when content is unchanged.
Downloads never touch the active file: GeoVault downloads to a .tmp file,
verifies the response and size, computes the SHA-256, validates the MMDB
structure, then atomically replaces the active database and switches the
reader. Any failure preserves the existing database.
Client is safe for concurrent use, and lookups never hold locks across
network operations — downloads never block IP lookups:
for i := 0; i < 100; i++ {
go geo.Lookup(ip) // concurrent lookups
}
// plus a background update, plus Close() — all safe togetherThe package is verified with go test -race.
GeoVault exposes typed sentinel errors:
geovault.ErrDatabaseNotFound
geovault.ErrDatabaseInvalid
geovault.ErrDownloadFailed
geovault.ErrDatabaseUpdateFailed
geovault.ErrClosed
geovault.ErrInvalidIPInspect them with errors.Is:
if _, err := geo.Lookup("8.8.8.8"); errors.Is(err, geovault.ErrDatabaseNotFound) {
// no database available
}No external logging framework is required. Pass any logger implementing the four-level interface:
type Logger interface {
Debug(msg string, args ...any)
Info(msg string, args ...any)
Warn(msg string, args ...any)
Error(msg string, args ...any)
}A no-op logger is used by default.