Kamero

IP Geolocation in Go: Get Location from IP Address

Go is a popular choice for building APIs, microservices, and backend systems. Adding IP geolocation to your Go application is straightforward with a free API — no external packages needed beyond the standard library.

Basic Example: net/http

package main

import (
    "encoding/json"
    "fmt"
    "net/http"
    "time"
)

type GeoLocation struct {
    IP            string `json:"ip"`
    City          string `json:"city"`
    Country       string `json:"country"`
    CountryRegion string `json:"countryRegion"`
    Continent     string `json:"continent"`
    Latitude      string `json:"latitude"`
    Longitude     string `json:"longitude"`
    Timezone      string `json:"timezone"`
    PostalCode    string `json:"postalCode"`
    Region        string `json:"region"`
}

func getGeolocation() (*GeoLocation, error) {
    client := &http.Client{Timeout: 5 * time.Second}

    resp, err := client.Get("https://geo.kamero.ai/api/geo")
    if err != nil {
        return nil, fmt.Errorf("request failed: %w", err)
    }
    defer resp.Body.Close()

    var geo GeoLocation
    if err := json.NewDecoder(resp.Body).Decode(&geo); err != nil {
        return nil, fmt.Errorf("decode failed: %w", err)
    }

    return &geo, nil
}

func main() {
    geo, err := getGeolocation()
    if err != nil {
        fmt.Printf("Error: %v\n", err)
        return
    }

    fmt.Printf("IP: %s\n", geo.IP)
    fmt.Printf("City: %s\n", geo.City)
    fmt.Printf("Country: %s\n", geo.Country)
    fmt.Printf("Timezone: %s\n", geo.Timezone)
    fmt.Printf("Coordinates: %s, %s\n", geo.Latitude, geo.Longitude)
}

Gin Middleware

If you're using Gin, add geolocation as middleware so it's available in every handler:

package middleware

import (
    "encoding/json"
    "net/http"
    "time"

    "github.com/gin-gonic/gin"
)

func GeoLocation() gin.HandlerFunc {
    client := &http.Client{Timeout: 5 * time.Second}

    return func(c *gin.Context) {
        resp, err := client.Get("https://geo.kamero.ai/api/geo")
        if err != nil {
            c.Set("geo", nil)
            c.Next()
            return
        }
        defer resp.Body.Close()

        var geo map[string]interface{}
        json.NewDecoder(resp.Body).Decode(&geo)
        c.Set("geo", geo)
        c.Next()
    }
}

// Usage:
// r := gin.Default()
// r.Use(middleware.GeoLocation())
//
// r.GET("/", func(c *gin.Context) {
//     geo, _ := c.Get("geo")
//     c.JSON(200, geo)
// })

With In-Memory Caching

Cache results using sync.Map to avoid redundant API calls:

package geo

import (
    "encoding/json"
    "net/http"
    "sync"
    "time"
)

type CachedGeo struct {
    Data      *GeoLocation
    ExpiresAt time.Time
}

var (
    cache  sync.Map
    client = &http.Client{Timeout: 5 * time.Second}
)

func Lookup(ip string) (*GeoLocation, error) {
    // Check cache
    if cached, ok := cache.Load(ip); ok {
        entry := cached.(*CachedGeo)
        if time.Now().Before(entry.ExpiresAt) {
            return entry.Data, nil
        }
        cache.Delete(ip)
    }

    // Fetch from API
    resp, err := client.Get("https://geo.kamero.ai/api/geo")
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()

    var geo GeoLocation
    if err := json.NewDecoder(resp.Body).Decode(&geo); err != nil {
        return nil, err
    }

    // Cache for 1 hour
    cache.Store(ip, &CachedGeo{
        Data:      &geo,
        ExpiresAt: time.Now().Add(time.Hour),
    })

    return &geo, nil
}

Concurrent Lookups

Go's goroutines make it easy to look up multiple IPs concurrently:

func lookupBatch(ips []string) []*GeoLocation {
    results := make([]*GeoLocation, len(ips))
    var wg sync.WaitGroup

    for i, ip := range ips {
        wg.Add(1)
        go func(idx int, addr string) {
            defer wg.Done()
            geo, err := Lookup(addr)
            if err == nil {
                results[idx] = geo
            }
        }(i, ip)
    }

    wg.Wait()
    return results
}

HTTP Handler: Expose as Your Own API

Wrap the geolocation lookup in your own HTTP handler:

func geoHandler(w http.ResponseWriter, r *http.Request) {
    geo, err := getGeolocation()
    if err != nil {
        http.Error(w, "Failed to get location", 500)
        return
    }

    w.Header().Set("Content-Type", "application/json")
    w.Header().Set("Access-Control-Allow-Origin", "*")
    json.NewEncoder(w).Encode(geo)
}

func main() {
    http.HandleFunc("/api/location", geoHandler)
    fmt.Println("Server running on :8080")
    http.ListenAndServe(":8080", nil)
}

Try the API

Standard JSON response. Works with any Go HTTP client.

Go Examples →