Skip to content

Commit cd9021f

Browse files
committed
Generate GeoIP2 model decoders
Generate unmarshaling methods for every exported database model using maxminddb-gen from maxminddb-golang v2.5.0. Keep the handwritten Date decoder and retain a pure-reflection benchmark baseline.
1 parent 09c8960 commit cd9021f

10 files changed

Lines changed: 2634 additions & 24 deletions

File tree

.github/workflows/go.yml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,24 @@ name: Go
33
on: [push, pull_request]
44

55
jobs:
6+
generated:
7+
name: Generated files
8+
runs-on: ubuntu-latest
9+
steps:
10+
- name: Set up Go
11+
uses: actions/setup-go@v6
12+
with:
13+
go-version: 1.25.x
14+
15+
- name: Check out code
16+
uses: actions/checkout@v6
17+
18+
- name: Regenerate models
19+
run: go generate ./...
20+
21+
- name: Verify generated files are current
22+
run: git diff --exit-code
23+
624
build:
725
name: Build
826
strategy:

.golangci.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ linters:
1616
- gochecknoglobals
1717
- gocognit
1818
- godox
19+
- gomodguard
1920
- gosmopolitan
2021
- inamedparam
2122
- interfacebloat
@@ -153,6 +154,13 @@ linters:
153154
check-exported: true
154155
exclusions:
155156
warn-unused: true
157+
rules:
158+
- linters:
159+
- recvcheck
160+
path: models.go
161+
# Generated unmarshaling must mutate pointer receivers, while the
162+
# existing model inspection methods intentionally use values.
163+
text: 'methods of ".*" use pointer receiver and non-pointer receiver'
156164
formatters:
157165
enable:
158166
- gci

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# Changes
22

3+
## Unreleased
4+
5+
- Updated `github.com/oschwald/maxminddb-golang/v2` to `v2.5.0` and switched
6+
all exported database model structs to generated decoders. This substantially
7+
reduces City and Enterprise lookup latency and allocations while preserving
8+
the existing lookup APIs and decoded results.
9+
310
## 2.2.0 - 2026-05-17
411

512
- This module now targets Go 1.25+.

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,16 @@ func main() {
104104
- Go 1.25 or later
105105
- MaxMind GeoIP2 or GeoLite2 database files (.mmdb format)
106106

107+
## Generated Decoders
108+
109+
Database model structs use decoders generated by the `maxminddb-gen` tool from
110+
`maxminddb-golang`. This is transparent to library users and improves lookup
111+
performance while preserving the decoded results.
112+
113+
When changing `models.go`, run `go generate ./...` and commit the updated
114+
`models_maxminddb.go`. The generator version is pinned in `go.mod`, and CI
115+
checks that generated files are current.
116+
107117
## Getting Database Files
108118

109119
### GeoLite2 (Free)

generated_test.go

Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
package geoip2
2+
3+
import (
4+
"net/netip"
5+
"testing"
6+
7+
"github.com/stretchr/testify/require"
8+
9+
"github.com/oschwald/maxminddb-golang/v2/mmdbdata"
10+
)
11+
12+
var (
13+
_ mmdbdata.CursorUnmarshaler = (*City)(nil)
14+
_ mmdbdata.CursorUnmarshaler = (*Enterprise)(nil)
15+
)
16+
17+
var (
18+
generatedCitySink *City
19+
reusedCitySink City
20+
benchmarkCitySink *cityReflectionBenchmark
21+
reusedBenchCitySink cityReflectionBenchmark
22+
generatedEntSink *Enterprise
23+
reusedEntSink Enterprise
24+
benchmarkEntSink *enterpriseReflectionBenchmark
25+
reusedBenchEntSink enterpriseReflectionBenchmark
26+
)
27+
28+
func TestGeneratedCityParity(t *testing.T) {
29+
reader, err := Open("test-data/test-data/GeoIP2-City-Test.mmdb")
30+
require.NoError(t, err)
31+
t.Cleanup(func() { require.NoError(t, reader.Close()) })
32+
33+
addresses := cityBenchmarkAddresses()
34+
var generated City
35+
var reflection cityReflectionBenchmark
36+
seed := reader.mmdbReader.Lookup(addresses[len(addresses)-1])
37+
require.NoError(t, seed.Decode(&generated))
38+
require.NoError(t, seed.Decode(&reflection))
39+
require.Equal(t, cityFromReflection(reflection), generated, "dirty-destination seed")
40+
for iteration := range 3 {
41+
for _, address := range addresses {
42+
result := reader.mmdbReader.Lookup(address)
43+
require.NoError(t, result.Decode(&generated))
44+
require.NoError(t, result.Decode(&reflection))
45+
require.Equal(t, cityFromReflection(reflection), generated,
46+
"iteration %d, address %s", iteration, address)
47+
}
48+
}
49+
}
50+
51+
func TestGeneratedEnterpriseParity(t *testing.T) {
52+
reader, err := Open("test-data/test-data/GeoIP2-Enterprise-Test.mmdb")
53+
require.NoError(t, err)
54+
t.Cleanup(func() { require.NoError(t, reader.Close()) })
55+
56+
addresses := []netip.Addr{
57+
netip.MustParseAddr("74.209.24.0"),
58+
netip.MustParseAddr("81.2.69.160"),
59+
netip.MustParseAddr("149.101.100.0"),
60+
netip.MustParseAddr("214.1.1.0"),
61+
}
62+
var generated Enterprise
63+
var reflection enterpriseReflectionBenchmark
64+
seed := reader.mmdbReader.Lookup(addresses[len(addresses)-1])
65+
require.NoError(t, seed.Decode(&generated))
66+
require.NoError(t, seed.Decode(&reflection))
67+
require.Equal(t, enterpriseFromReflection(reflection), generated, "dirty-destination seed")
68+
for iteration := range 3 {
69+
for _, address := range addresses {
70+
result := reader.mmdbReader.Lookup(address)
71+
require.NoError(t, result.Decode(&generated))
72+
require.NoError(t, result.Decode(&reflection))
73+
require.Equal(t, enterpriseFromReflection(reflection), generated,
74+
"iteration %d, address %s", iteration, address)
75+
}
76+
}
77+
}
78+
79+
func BenchmarkCityDecodeGeneratedReused(b *testing.B) {
80+
reader, err := Open("test-data/test-data/GeoIP2-City-Test.mmdb")
81+
require.NoError(b, err)
82+
b.Cleanup(func() { require.NoError(b, reader.Close()) })
83+
addresses := cityBenchmarkAddresses()
84+
results := make([]resultDecode, len(addresses))
85+
for i, address := range addresses {
86+
result := reader.mmdbReader.Lookup(address)
87+
results[i] = resultDecode{decode: result.Decode}
88+
}
89+
var city City
90+
b.ReportAllocs()
91+
b.ResetTimer()
92+
for i := 0; b.Loop(); i++ {
93+
if err := results[i%len(results)].decode(&city); err != nil {
94+
b.Fatal(err)
95+
}
96+
}
97+
reusedCitySink = city
98+
}
99+
100+
func BenchmarkCityDecodeReflectionReused(b *testing.B) {
101+
reader, err := Open("test-data/test-data/GeoIP2-City-Test.mmdb")
102+
require.NoError(b, err)
103+
b.Cleanup(func() { require.NoError(b, reader.Close()) })
104+
addresses := cityBenchmarkAddresses()
105+
results := make([]resultDecode, len(addresses))
106+
for i, address := range addresses {
107+
result := reader.mmdbReader.Lookup(address)
108+
results[i] = resultDecode{decode: result.Decode}
109+
}
110+
var city cityReflectionBenchmark
111+
b.ReportAllocs()
112+
b.ResetTimer()
113+
for i := 0; b.Loop(); i++ {
114+
if err := results[i%len(results)].decode(&city); err != nil {
115+
b.Fatal(err)
116+
}
117+
}
118+
reusedBenchCitySink = city
119+
}
120+
121+
func BenchmarkCityLookupGeneratedFresh(b *testing.B) {
122+
reader, err := Open("test-data/test-data/GeoIP2-City-Test.mmdb")
123+
require.NoError(b, err)
124+
b.Cleanup(func() { require.NoError(b, reader.Close()) })
125+
addresses := cityBenchmarkAddresses()
126+
var city *City
127+
b.ReportAllocs()
128+
b.ResetTimer()
129+
for i := 0; b.Loop(); i++ {
130+
city, err = reader.City(addresses[i%len(addresses)])
131+
if err != nil {
132+
b.Fatal(err)
133+
}
134+
}
135+
generatedCitySink = city
136+
}
137+
138+
func BenchmarkCityLookupReflectionFresh(b *testing.B) {
139+
reader, err := Open("test-data/test-data/GeoIP2-City-Test.mmdb")
140+
require.NoError(b, err)
141+
b.Cleanup(func() { require.NoError(b, reader.Close()) })
142+
addresses := cityBenchmarkAddresses()
143+
var city *cityReflectionBenchmark
144+
b.ReportAllocs()
145+
b.ResetTimer()
146+
for i := 0; b.Loop(); i++ {
147+
address := addresses[i%len(addresses)]
148+
result := reader.mmdbReader.Lookup(address)
149+
city = new(cityReflectionBenchmark)
150+
if err = result.Decode(city); err != nil {
151+
b.Fatal(err)
152+
}
153+
city.Traits.IPAddress = address
154+
city.Traits.Network = result.Prefix()
155+
}
156+
benchmarkCitySink = city
157+
}
158+
159+
func BenchmarkCityCommercialGeneratedFresh(b *testing.B) {
160+
reader, err := Open("/var/lib/GeoIP/GeoIP2-City.mmdb")
161+
if err != nil {
162+
b.Skip(err)
163+
}
164+
b.Cleanup(func() { require.NoError(b, reader.Close()) })
165+
address := netip.MustParseAddr("128.101.101.101")
166+
var city *City
167+
b.ReportAllocs()
168+
b.ResetTimer()
169+
for b.Loop() {
170+
city, err = reader.City(address)
171+
if err != nil {
172+
b.Fatal(err)
173+
}
174+
}
175+
generatedCitySink = city
176+
}
177+
178+
func BenchmarkEnterpriseDecodeGeneratedReused(b *testing.B) {
179+
reader, err := Open("test-data/test-data/GeoIP2-Enterprise-Test.mmdb")
180+
require.NoError(b, err)
181+
b.Cleanup(func() { require.NoError(b, reader.Close()) })
182+
addresses := enterpriseBenchmarkAddresses()
183+
results := make([]resultDecode, len(addresses))
184+
for i, address := range addresses {
185+
result := reader.mmdbReader.Lookup(address)
186+
results[i] = resultDecode{decode: result.Decode}
187+
}
188+
var enterprise Enterprise
189+
b.ReportAllocs()
190+
b.ResetTimer()
191+
for i := 0; b.Loop(); i++ {
192+
if err := results[i%len(results)].decode(&enterprise); err != nil {
193+
b.Fatal(err)
194+
}
195+
}
196+
reusedEntSink = enterprise
197+
}
198+
199+
func BenchmarkEnterpriseDecodeReflectionReused(b *testing.B) {
200+
reader, err := Open("test-data/test-data/GeoIP2-Enterprise-Test.mmdb")
201+
require.NoError(b, err)
202+
b.Cleanup(func() { require.NoError(b, reader.Close()) })
203+
addresses := enterpriseBenchmarkAddresses()
204+
results := make([]resultDecode, len(addresses))
205+
for i, address := range addresses {
206+
result := reader.mmdbReader.Lookup(address)
207+
results[i] = resultDecode{decode: result.Decode}
208+
}
209+
var enterprise enterpriseReflectionBenchmark
210+
b.ReportAllocs()
211+
b.ResetTimer()
212+
for i := 0; b.Loop(); i++ {
213+
if err := results[i%len(results)].decode(&enterprise); err != nil {
214+
b.Fatal(err)
215+
}
216+
}
217+
reusedBenchEntSink = enterprise
218+
}
219+
220+
func BenchmarkEnterpriseLookupGeneratedFresh(b *testing.B) {
221+
reader, err := Open("test-data/test-data/GeoIP2-Enterprise-Test.mmdb")
222+
require.NoError(b, err)
223+
b.Cleanup(func() { require.NoError(b, reader.Close()) })
224+
addresses := enterpriseBenchmarkAddresses()
225+
var enterprise *Enterprise
226+
b.ReportAllocs()
227+
b.ResetTimer()
228+
for i := 0; b.Loop(); i++ {
229+
enterprise, err = reader.Enterprise(addresses[i%len(addresses)])
230+
if err != nil {
231+
b.Fatal(err)
232+
}
233+
}
234+
generatedEntSink = enterprise
235+
}
236+
237+
func BenchmarkEnterpriseLookupReflectionFresh(b *testing.B) {
238+
reader, err := Open("test-data/test-data/GeoIP2-Enterprise-Test.mmdb")
239+
require.NoError(b, err)
240+
b.Cleanup(func() { require.NoError(b, reader.Close()) })
241+
addresses := enterpriseBenchmarkAddresses()
242+
var enterprise *enterpriseReflectionBenchmark
243+
b.ReportAllocs()
244+
b.ResetTimer()
245+
for i := 0; b.Loop(); i++ {
246+
address := addresses[i%len(addresses)]
247+
result := reader.mmdbReader.Lookup(address)
248+
enterprise = new(enterpriseReflectionBenchmark)
249+
if err = result.Decode(enterprise); err != nil {
250+
b.Fatal(err)
251+
}
252+
enterprise.Traits.IPAddress = address
253+
enterprise.Traits.Network = result.Prefix()
254+
}
255+
benchmarkEntSink = enterprise
256+
}
257+
258+
type resultDecode struct {
259+
decode func(any) error
260+
}
261+
262+
func cityBenchmarkAddresses() []netip.Addr {
263+
return []netip.Addr{
264+
netip.MustParseAddr("2.125.160.216"),
265+
netip.MustParseAddr("67.43.156.0"),
266+
netip.MustParseAddr("81.2.69.160"),
267+
netip.MustParseAddr("89.160.20.128"),
268+
netip.MustParseAddr("175.16.199.0"),
269+
netip.MustParseAddr("187.44.1.0"),
270+
netip.MustParseAddr("202.196.224.0"),
271+
netip.MustParseAddr("216.160.83.56"),
272+
}
273+
}
274+
275+
func enterpriseBenchmarkAddresses() []netip.Addr {
276+
return []netip.Addr{
277+
netip.MustParseAddr("74.209.24.0"),
278+
netip.MustParseAddr("81.2.69.160"),
279+
netip.MustParseAddr("149.101.100.0"),
280+
netip.MustParseAddr("175.16.199.0"),
281+
netip.MustParseAddr("187.44.1.0"),
282+
netip.MustParseAddr("202.196.224.0"),
283+
netip.MustParseAddr("214.1.1.0"),
284+
netip.MustParseAddr("216.160.83.56"),
285+
}
286+
}

go.mod

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,18 @@ module github.com/oschwald/geoip2-golang/v2
33
go 1.25.0
44

55
require (
6-
github.com/oschwald/maxminddb-golang/v2 v2.4.1
6+
github.com/oschwald/maxminddb-golang/v2 v2.5.0
77
github.com/stretchr/testify v1.11.1
88
)
99

10+
tool github.com/oschwald/maxminddb-golang/v2/maxminddb-gen
11+
1012
require (
1113
github.com/davecgh/go-spew v1.1.1 // indirect
1214
github.com/pmezard/go-difflib v1.0.0 // indirect
13-
golang.org/x/sys v0.46.0 // indirect
15+
golang.org/x/mod v0.38.0 // indirect
16+
golang.org/x/sync v0.22.0 // indirect
17+
golang.org/x/sys v0.47.0 // indirect
18+
golang.org/x/tools v0.48.0 // indirect
1419
gopkg.in/yaml.v3 v3.0.1 // indirect
1520
)

0 commit comments

Comments
 (0)