GeoIP Lookup Service with MaxMind Database
Memory-caches MaxMind GeoLite2 data to serve fast IP-to-location lookups with IPv4 and IPv6 support.
main.go
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
package main
import (
"fmt"
"log"
"net"
"net/http"
"os"
"sync"
"github.com/oschwald/geoip2-golang"
)
// GeoIPService provides thread-safe GeoIP lookup functionality
type GeoIPService struct {
db *geoip2.Reader
mu sync.RWMutex
loaded bool
dbPath string
}
// NewGeoIPService creates a new service with a MaxMind database path
func NewGeoIPService(dbPath string) *GeoIPService {
return &GeoIPService{
dbPath: dbPath,
}
}
// LoadDatabase initializes the GeoIP database (call once at startup)
func (s *GeoIPService) LoadDatabase() error {
s.mu.Lock()
defer s.mu.Unlock()
if s.loaded {
return nil
}
// Open MaxMind database file
dbFile, err := os.Open(s.dbPath)
if err != nil {
return fmt.Errorf("failed to open GeoIP database: %v", err)
}
// Initialize GeoIP reader
db, err := geoip2.NewReader(dbFile)
if err != nil {
dbFile.Close()
return fmt.Errorf("failed to initialize GeoIP reader: %v", err)
}
s.db = db
s.loaded = true
log.Printf("Successfully loaded GeoIP database: %s", s.dbPath)
return nil
}
// LookupCity retrieves city-level geolocation data for an IP address
func (s *GeoIPService) LookupCity(ipStr string) (*geoip2.City, error) {
s.mu.RLock()
defer s.mu.RUnlock()
if !s.loaded {
return nil, fmt.Errorf("GeoIP database not loaded")
}
// Parse IP address
ip := net.ParseIP(ipStr)
if ip == nil {
return nil, fmt.Errorf("invalid IP address: %s", ipStr)
}
// Perform lookup
record, err := s.db.City(ip)
if err != nil {
return nil, fmt.Errorf("lookup failed: %v", err)
}
return record, nil
}
// Close cleans up resources by closing the GeoIP database
func (s *GeoIPService) Close() error {
s.mu.Lock()
defer s.mu.Unlock()
if !s.loaded {
return nil
}
err := s.db.Close()
s.loaded = false
s.db = nil
log.Println("GeoIP database closed")
return err
}
// HTTP handler for GeoIP lookup API
func (s *GeoIPService) lookupHandler(w http.ResponseWriter, r *http.Request) {
// Extract IP from query parameter
ipStr := r.URL.Query().Get("ip")
if ipStr == "" {
host, _, err := net.SplitHostPort(r.RemoteAddr)
if err == nil {
ipStr = host
} else {
ipStr = r.RemoteAddr
}
}
// Perform lookup
record, err := s.LookupCity(ipStr)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// Format response as JSON
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{
"ip": "%s",
"country": "%s",
"country_code": "%s",
"city": "%s",
"latitude": %f,
"longitude": %f,
"timezone": "%s"
}`,
ipStr,
record.Country.Names["en"],
record.Country.IsoCode,
record.City.Names["en"],
record.Location.Latitude,
record.Location.Longitude,
record.Location.TimeZone,
)
}
// Example Usage
func main() {
if len(os.Args) < 2 {
log.Fatal("Usage: go run main.go <GeoLite2-City.mmdb-path>")
}
dbPath := os.Args[1]
// Create and initialize GeoIP service
service := NewGeoIPService(dbPath)
if err := service.LoadDatabase(); err != nil {
log.Fatalf("Failed to load GeoIP database: %v", err)
}
defer service.Close()
// Register HTTP handler
http.HandleFunc("/api/geoip", service.lookupHandler)
// Start HTTP server
log.Println("GeoIP lookup service starting on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}How It Works
Memory-caches MaxMind GeoLite2 data to serve fast IP-to-location lookups with IPv4 and IPv6 support.
Loads the MaxMind database into memory, exposes lookup functions to parse IPs, retrieves country and city data, and returns structured responses with graceful errors for unknown addresses.
Key Concepts
- 1Thread-safe reader allows concurrent lookups without locks.
- 2Supports both IPv4 and IPv6 queries.
- 3Returns structured geo fields for downstream use.
When to Use This Pattern
- Personalization or localization by client IP.
- Fraud or risk scoring pipelines.
- Geo-aware analytics and reporting.
Best Practices
- Update GeoLite databases regularly to keep accuracy high.
- Handle private or reserved IPs gracefully.
- Cache parsed responses if lookups become hot paths.
Go Version1.17
Difficultyintermediate
Production ReadyYes
Lines of Code154