This is a Go reader for the MaxMind DB format. Although this can be used to read GeoLite2 and GeoIP2 databases, geoip2 provides a higher-level API for doing so.
This is not an official MaxMind API.
go get github.com/oschwald/maxminddb-golang/v2Version 2 includes significant improvements:
- Modern API: Uses
netip.Addrinstead ofnet.IPfor better performance - Custom Unmarshaling: Implement
CursorUnmarshalerfor reflection-free custom decoding - Network Iteration: Iterate over all networks in a database with
Networks()andNetworksWithin() - Enhanced Performance: Optimized data structures and decoding paths
- Better Error Handling: More detailed error types and improved debugging
- Integrity Checks: Validate databases with
Reader.Verify()and access metadata helpers such asMetadata.BuildTime()
See MIGRATION.md for guidance on updating existing v1 code.
package main
import (
"fmt"
"log"
"net/netip"
"github.com/oschwald/maxminddb-golang/v2"
)
func main() {
db, err := maxminddb.Open("GeoLite2-City.mmdb")
if err != nil {
log.Fatal(err)
}
defer db.Close()
ip, err := netip.ParseAddr("81.2.69.142")
if err != nil {
log.Fatal(err)
}
var record struct {
Country struct {
ISOCode string `maxminddb:"iso_code"`
Names map[string]string `maxminddb:"names"`
} `maxminddb:"country"`
City struct {
Names map[string]string `maxminddb:"names"`
} `maxminddb:"city"`
}
err = db.Lookup(ip).Decode(&record)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Country: %s (%s)\n", record.Country.Names["en"], record.Country.ISOCode)
fmt.Printf("City: %s\n", record.City.Names["en"])
}db, err := maxminddb.Open("GeoLite2-City.mmdb")
if err != nil {
log.Fatal(err)
}
defer db.Close()
var record any
ip := netip.MustParseAddr("1.2.3.4")
err = db.Lookup(ip).Decode(&record)Call Reader.Verify once immediately after opening an untrusted database and
before performing lookups or decoding records:
if err := db.Verify(); err != nil {
log.Fatal(err)
}Verification applies to the database contents at the time of the call. Keep
those contents immutable for the Reader's lifetime: do not modify a slice
passed to OpenBytes or rewrite or truncate a memory-mapped file in place.
Open and verify a new Reader when publishing an updated database.
Reflection decoding limits each operation to 32,768 declared container child
slots; map keys and values each consume one slot. Maps and slices reserve their
children before allocation or traversal. Materialized string and byte payloads
also have an operation-wide bound: every delivered payload byte draws from a
shared 2 MiB allowance. Materialized map keys and keys inspected by DecodePath
draw their full size from the same payload allowance.
Decoding into any activates these limits even when the root is a scalar. A
standalone scalar decoded into a directly typed destination or a named
empty-interface type remains unbudgeted because it cannot amplify.
A non-empty DecodePath shares one set of limits between path navigation and
the selected value. Skipping an unknown field still charges any inline
containers, but does not follow pointer targets or charge payload that is not
materialized. Custom unmarshalers and low-level cursor traversal control and
must bound their own work; Reader.Verify validates the complete data section
and the original metadata graph, including unknown metadata fields, before
those APIs are used with untrusted input.
type City struct {
Country struct {
ISOCode string `maxminddb:"iso_code"`
Names struct {
English string `maxminddb:"en"`
German string `maxminddb:"de"`
} `maxminddb:"names"`
} `maxminddb:"country"`
Subdivisions []struct {
ISOCode string `maxminddb:"iso_code"`
} `maxminddb:"subdivisions,maxsize:32"`
}
var city City
err = db.Lookup(ip).Decode(&city)The maxsize:N tag option rejects a matching MMDB map or array with more than
N entries, or a matching string or byte value with more than N bytes. An
MMDB array decoded into []byte is covered as well. The check happens before
the matching field is allocated or mutated and is supported by both reflection
decoding and maxminddb-gen. Tag options use the encoding/json/v2 comma and
colon grammar, for example maxminddb:"subdivisions,maxsize:32". Because a
comma delimits options, quote a literal field name containing a comma with the
same grammar, for example maxminddb:"'city,name'". For a supported custom
field type, maxsize checks every size-bearing MMDB kind (map, array, string,
and bytes) before invoking the unmarshaler because the encodings accepted by a
callback cannot be inferred from its Go type.
For application-owned structs, maxminddb-gen can generate an
UnmarshalMaxMindDBCursor method that avoids reflection. The generator is
versioned with this module and remains optional; types with neither generated
nor handwritten custom unmarshaling methods continue to use reflection.
Add the tool to the consuming module's go.mod and add a generation directive
in the package that owns the target types:
tool github.com/oschwald/maxminddb-golang/v2/maxminddb-gen//go:generate go tool maxminddb-gen $GOFILEThis discovers the exported structs declared in the directive's source file.
For models.go, it writes models_maxminddb.go; recognized build suffixes and
source build constraints are preserved. Constrained inputs must match the
generation environment; multiple inputs share the intersection of their
constraints. Use -output to override the default. Run go generate ./... and
check the generated file into source control. See
maxminddb-gen/README.md for supported types,
diagnostics, and reproducible CI usage.
For new handwritten decoders, implement mmdbdata.CursorUnmarshaler. Cursor
reads return an opaque successor positioned after the decoded value, allowing
nested custom decoding to continue without rescanning it.
The older UnmarshalMaxMindDB(*mmdbdata.Decoder) error callback is deprecated.
It remains supported throughout v2 but is planned for removal in v3; see
GitHub #224. When a
type implements both callbacks, UnmarshalMaxMindDBCursor takes precedence.
Custom unmarshalers control their own traversal and allocation. If a database is not trusted, an implementation should use one aggregate per-record work budget across nested calls. The budget should cover recursion, collection entries, repeated pointer targets, and produced string or byte payloads; the reflection decoder's expansion guard is not applied inside custom callbacks.
type Label string
func (label *Label) UnmarshalMaxMindDBCursor(
cursor mmdbdata.Cursor,
) (mmdbdata.Cursor, error) {
value, next, err := cursor.ReadString()
if err != nil {
return mmdbdata.Cursor{}, mmdbdata.NormalizeUnmarshalError[Label](err)
}
*label = Label(value)
return next, nil
}// Iterate over all networks in the database
for result := range db.Networks() {
var record struct {
Country struct {
ISOCode string `maxminddb:"iso_code"`
} `maxminddb:"country"`
}
err := result.Decode(&record)
if err != nil {
log.Fatal(err)
}
fmt.Printf("%s: %s\n", result.Prefix(), record.Country.ISOCode)
}
// Iterate over networks within a specific prefix
prefix := netip.MustParsePrefix("192.168.0.0/16")
for result := range db.NetworksWithin(prefix) {
// Process networks within 192.168.0.0/16
}var countryCode string
err = db.Lookup(ip).DecodePath(&countryCode, "country", "iso_code")
var cityName string
err = db.Lookup(ip).DecodePath(&cityName, "city", "names", "en")This library supports all MaxMind DB (.mmdb) format databases, including:
MaxMind Official Databases:
- GeoLite/GeoIP City: Comprehensive location data including city, country, subdivisions
- GeoLite/GeoIP Country: Country-level geolocation data
- GeoLite ASN: Autonomous System Number and organization data
- GeoIP Anonymous IP: Anonymous network and proxy detection
- GeoIP Enterprise: Enhanced City data with additional business fields
- GeoIP ISP: Internet service provider information
- GeoIP Domain: Second-level domain data
- GeoIP Connection Type: Connection type identification
Third-Party Databases:
- DB-IP databases: Compatible with DB-IP's .mmdb format databases
- IPinfo databases: Works with IPinfo's MaxMind DB format files
- Custom databases: Any database following the MaxMind DB file format specification
The library is format-agnostic and will work with any valid .mmdb file regardless of the data provider.
- Reuse Reader instances: Lookups, decoding, and iteration are safe to run
concurrently.
Closeinvalidates outstanding results, Reader-backed cursors, and their derived traversal handles; it must not run concurrently with their use and should run only after readers are done. - Use specific structs: Only decode the fields you need rather than using
any - Generate a decoder: For high-throughput applications, use
maxminddb-gen, or implementCursorUnmarshalerfor custom decoding - Consider caching: Use
Result.Offset()as a cache key for database records
Standalone mmdbdata.NewDecoder calls do not cache strings by default. For a
decoder that repeatedly reads the same strings, pass mmdbdata.WithStringCache().
The decoder and its cursors share the cache. Use decoder.CursorAt(offset) to
read different records from the same buffer without constructing a new decoder
or changing its position. Independent cursors can be read concurrently. Keep the
input buffer unchanged while a cached decoder or any of its cursors are in use.
Separate NewDecoder calls do not share caches.
On 64-bit systems, the table allocates 72 KiB when the first string of 2 through 100 bytes is read, plus memory for retained strings. Reader-supplied decoders and cursors use the Reader's caching policy.
Download from MaxMind's GeoLite page.
- Go 1.26 or later
- MaxMind DB file in .mmdb format
Contributions welcome! Please fork the repository and open a pull request with your changes.
The real-City benchmarks use GeoLite2-City.mmdb in the current directory or
the path in MAXMIND_REAL_CITY_DB. They need a full City database and skip when
the default file is absent. An invalid explicit path fails the benchmark.
Measure lookup time and allocations without instrumentation:
MAXMIND_REAL_CITY_DB=/path/to/GeoIP2-City.mmdb go test -run '^$' -bench '^BenchmarkRealCityCache$' -benchmemMeasure cache hit rates separately:
MAXMIND_REAL_CITY_DB=/path/to/GeoIP2-City.mmdb go test -tags cachemetrics -run '^$' -bench '^BenchmarkRealCityCacheHitRate$'The cachemetrics build adds shared counters and suppresses benchmark timing.
Use its hit-rate results separately from uninstrumented timings.
BenchmarkStringCacheHotHome exercises the home-slot fast path;
BenchmarkStringCacheHotDisplaced exercises bucket scanning. Neither
microbenchmark represents a full database lookup.
This is free software, licensed under the ISC License.