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