Install maxminddb-golang/v2
mainInstall the MaxMind DB reader for Go using go get:
go get github.com/oschwald/maxminddb-golang/v2repository·main·Indexed 20 days ago
https://github.com/oschwald/maxminddb-golangA high-performance Go reader for the MaxMind DB (.mmdb) format, supporting GeoLite2, GeoIP2, and other third-party databases. It utilizes netip.Addr for efficiency and provides features such as zero-allocation custom unmarshaling via the Unmarshaler interface, path-based decoding with DecodePath, and network iteration compatible with Go 1.23+ range syntax.
Install the MaxMind DB reader for Go using go get:
go get github.com/oschwald/maxminddb-golang/v2To perform a basic lookup, open a database file using maxminddb.Open, parse an IP address using netip.ParseAddr, and decode the result into a struct using db.Lookup(ip).Decode(&record). Note that this library uses netip.Addr for improved performance over the standard net.IP.
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"])
}To optimize performance when using this library:
Close only when all readers are finished.any; define structs with only the fields you need.Unmarshaler interface for high-throughput requirements to minimize allocations.Result.Offset() as a cache key to store and retrieve database records efficiently.When upgrading to v2, update your import path to include the /v2 suffix.
// Old v1 import
import "github.com/oschwald/maxminddb-golang"
// New v2 import
import "github.com/oschwald/maxminddb-golang/v2"import "github.com/oschwald/maxminddb-golang/v2"For high-performance requirements, you can implement the mmdbdata.Unmarshaler interface on your custom types. This allows you to bypass reflection and use fine-grained decoding methods like d.ReadMap() or d.ReadString() directly.
type FastCity struct {
CountryISO string
CityName string
}
func (c *FastCity) UnmarshalMaxMindDB(d *mmdbdata.Decoder) error {
// Custom decoding logic using d.ReadMap(), d.ReadString(), etc.
return nil
}When iterating over networks using Networks or NetworksWithin, each step yields a Result object. A Result contains the following fields (accessible via methods or fields depending on the implementation):
IP: The netip.Addr of the network.Offset: The uint offset in the database where the data starts.PrefixLen: The uint8 length of the network prefix.Err: An error if the iteration failed or if the specific network encountered an issue.Reader: A reference to the *Reader used for the iteration.In v2, Reader.Networks() and Reader.NetworksWithin() yield iterators compatible with Go 1.23+ range syntax. This replaces the old Next()/Network() pattern. The Decode method on the yielded result handles errors, which are returned by the range loop.
Options for iteration (like SkipAliasedNetworks) now use an options pattern passed to the method.
for result := range reader.Networks() {
var record struct {
ConnectionType string `maxminddb:"connection_type"`
}
if err := result.Decode(&record); err != nil {
return err
}
fmt.Println(result.Prefix(), record.ConnectionType)
}You can map database fields to Go struct fields using the maxminddb struct tag. This allows you to extract only the specific data you need from the database record.
type City struct {
Country struct {
ISOCode string `maxminddb:"iso_code"`
Names struct {
English string `maxminddb:"en"`
German string `maxminddb:"de"`
} `maxminddb:"names"`
} `maxminddb:"country"`
}
var city City
err = db.Lookup(ip).Decode(&city)If you only need a single value from a deep path in the database, use DecodePath. This avoids the need to define complex nested structs for simple lookups.
var countryCode string
err = db.Lookup(ip).DecodePath(&countryCode, "country", "iso_code")
var cityName string
err = db.Lookup(ip).DecodePath(&cityName, "city", "names", "en")The Lookup method signature has changed. Instead of passing a destination pointer directly to Lookup, you now call Lookup with a netip.Addr and chain the .Decode() or .DecodePath() method to the returned Result object.
Migration steps:
net.IP inputs with net/netip.Addr (use netip.ParseAddr or addr.AsSlice() for interoperability).reader.Lookup(ip, &result) to reader.Lookup(addr).Decode(&result).// v1 pattern
err := reader.Lookup(net.IP, &result)
// v2 pattern
err := reader.Lookup(netip.Addr).Decode(&result)Reader.Verify() to validate the database structure and metadata. If corruption is detected, it returns an InvalidDatabaseError containing precise offset and JSON Pointer style path clues to assist in debugging.The Result.DecodePath method supports negative indices for arrays, allowing you to access elements from the end of a slice, similar to standard Go slice behavior.
Example: result.DecodePath(&value, "array", -1) fetches the last element of the array.