Go ISC

maxminddb-golang

MaxMind DB Reader for Go

O

oschwald

Dernière activité 29 sept. 2026
oschwald/maxminddb-golang

752

étoiles

108

forks

18

issues ouvertes

geoipgeoip2geolocationgoip-addressmaxmindmaxmind-db

Ce README est souvent en anglais.

MaxMind DB Reader for Go

Go Reference

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.

Installation

go get github.com/oschwald/maxminddb-golang/v2

Version 2 Features

Version 2 includes significant improvements:

  • Modern API: Uses netip.Addr instead of net.IP for better performance
  • Custom Unmarshaling: Implement CursorUnmarshaler for reflection-free custom decoding
  • Network Iteration: Iterate over all networks in a database with Networks() and NetworksWithin()
  • 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 as Metadata.BuildTime()

See MIGRATION.md for guidance on updating existing v1 code.

Quick Start

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"])
}

Usage Patterns

Basic Lookup

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)

Untrusted Database Files

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.

Custom Struct Decoding

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.

High-Performance Custom Unmarshaling

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 $GOFILE

This 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
}

Network Iteration

// 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
}

Path-Based Decoding

var countryCode string
err = db.Lookup(ip).DecodePath(&countryCode, "country", "iso_code")

var cityName string
err = db.Lookup(ip).DecodePath(&cityName, "city", "names", "en")

Supported Database Types

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.

Performance Tips

  1. Reuse Reader instances: Lookups, decoding, and iteration are safe to run concurrently. Close invalidates 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.
  2. Use specific structs: Only decode the fields you need rather than using any
  3. Generate a decoder: For high-throughput applications, use maxminddb-gen, or implement CursorUnmarshaler for custom decoding
  4. 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.

Getting Database Files

Free GeoLite2 Databases

Download from MaxMind's GeoLite page.

Documentation

Requirements

  • Go 1.26 or later
  • MaxMind DB file in .mmdb format

Contributing

Contributions welcome! Please fork the repository and open a pull request with your changes.

Cache Benchmarks

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$' -benchmem

Measure 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.

License

This is free software, licensed under the ISC License.

Projets similaires

Unofficial MaxMind GeoIP2 Reader for Go

Godatabasegeoipgeoip2
Ooschwald
2,3 k étoiles220

PHP Reader for the MaxMind DB Database Format

PHPgeoipgeoip2maxmind
Mmaxmind
705 étoiles83

MaxMind's GeoIP2 GeoLite2 Country, City, and ASN databases

geoipgeoip2geolocation
PP3TERX
5,3 k étoiles547