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)
}