
Intro #
A Geo API in Go provides a fast and efficient way to retrieve IP geolocation information such as country, ISP, usage type, and ASN in real time. However, when an application handles a large number of simultaneous requests, IP database lookups need to be implemented efficiently to avoid becoming a performance bottleneck.
Go is well suited for this type of workload because each incoming HTTP request can be handled concurrently using goroutines. In addition, when using the IP2Location Go module, multiple requests can query a single opened IP2Location BIN database without requiring a separate database connection for every request.
Internally, the IP2Location Go SDK performs positional reads against the BIN database using Go’s ReaderAt interface. This allows lookups to access the required portions of the database file independently, making the approach suitable for concurrent IP lookup workloads.
This guide walks through setting up a high-throughput HTTP Geolocation API on Debian 13 using the IP2Location DB26 IPv6 BIN file.
Prerequisites & Environment Setup #
Step 1: Install Go and Dependencies #
Install Go along with curl and jq for testing:
sudo apt update
sudo apt install -y golang curl jq
Verify your Go installation and architecture:
go version

Step 2: Set Up the Project Directory #
Create a directory for your service and initialize a Go module:
mkdir -p ~/geo-api && cd ~/geo-api
go mod init geo-api
Install the IP2Location Go package:
go get github.com/ip2location/ip2location-go/v9

Step 3: Place the DB26 BIN File #
Obtain your IP2Location DB26 IPv6 BIN file from your IP2Location account and copy it into your project folder with the filename IP2LOCATION-DB26.IPV6.BIN:
Verify the file is present in ~/geo-api:
ls -lh IP2LOCATION-DB26.IPV6.BIN

Writing the High-Concurrency Go HTTP API #
Create a file named main.go and paste the following code into it. Then save the file.
package main
import (
"encoding/json"
"log"
"net"
"net/http"
"strings"
"github.com/ip2location/ip2location-go/v9"
)
// Global database handle shared across all HTTP goroutines
var db *ip2location.DB
// GeoResponse maps all DB26 fields returned by ip2location-go
type GeoResponse struct {
IP string `json:"ip"`
CountryShort string `json:"country_short"`
CountryLong string `json:"country_long"`
Region string `json:"region"`
City string `json:"city"`
ISP string `json:"isp"`
Latitude float32 `json:"latitude"`
Longitude float32 `json:"longitude"`
Domain string `json:"domain"`
ZipCode string `json:"zip_code"`
TimeZone string `json:"time_zone"`
NetSpeed string `json:"net_speed"`
IDDCode string `json:"idd_code"`
AreaCode string `json:"area_code"`
WeatherStationCode string `json:"weather_station_code"`
WeatherStationName string `json:"weather_station_name"`
MCC string `json:"mcc"`
MNC string `json:"mnc"`
MobileBrand string `json:"mobile_brand"`
Elevation float32 `json:"elevation"`
UsageType string `json:"usage_type"`
AddressType string `json:"address_type"`
Category string `json:"category"`
District string `json:"district"`
ASN string `json:"asn"`
AS string `json:"as"`
ASDomain string `json:"as_domain"`
ASUsageType string `json:"as_usage_type"`
ASCIDR string `json:"as_cidr"`
}
func geoHandler(w http.ResponseWriter, r *http.Request) {
// 1. Extract IP from query parameter ?ip=x.x.x.x
ip := r.URL.Query().Get("ip")
// 2. Fall back to client's RemoteAddr if query parameter is absent
if ip == "" {
host, _, err := net.SplitHostPort(r.RemoteAddr)
if err == nil {
ip = host
} else {
ip = r.RemoteAddr
}
}
ip = strings.Trim(ip, "[]")
// 3. Thread-safe lookup across concurrent goroutines
rec, err := db.Get_all(ip)
if err != nil {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(map[string]string{"error": "Invalid IP address or lookup failed"})
return
}
// 4. Map all 28 DB26 fields to response struct
res := GeoResponse{
IP: ip,
CountryShort: rec.Country_short,
CountryLong: rec.Country_long,
Region: rec.Region,
City: rec.City,
ISP: rec.Isp,
Latitude: rec.Latitude,
Longitude: rec.Longitude,
Domain: rec.Domain,
ZipCode: rec.Zipcode,
TimeZone: rec.Timezone,
NetSpeed: rec.Netspeed,
IDDCode: rec.Iddcode,
AreaCode: rec.Areacode,
WeatherStationCode: rec.Weatherstationcode,
WeatherStationName: rec.Weatherstationname,
MCC: rec.Mcc,
MNC: rec.Mnc,
MobileBrand: rec.Mobilebrand,
Elevation: rec.Elevation,
UsageType: rec.Usagetype,
AddressType: rec.Addresstype,
Category: rec.Category,
District: rec.District,
ASN: rec.Asn,
AS: rec.As,
ASDomain: rec.Asdomain,
ASUsageType: rec.Asusagetype,
ASCIDR: rec.Ascidr,
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(res)
}
func main() {
var err error
// Initialize database ONCE at application startup.
// Opens a single file handle using zero-copy positional reads (io.ReaderAt).
db, err = ip2location.OpenDB("./IP2LOCATION-DB26.IPV6.BIN")
if err != nil {
log.Fatalf("Fatal: Unable to open DB26 BIN file: %v", err)
}
defer db.Close()
http.HandleFunc("/geo", geoHandler)
log.Println("Geo API server active on http://0.0.0.0:8080/geo")
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatalf("Server shutdown error: %v", err)
}
}
Running and Testing the API #
Start the API Server #
In your terminal, run the application:
go run main.go
Test 1: Querying Specific IP Addresses with curl #
Open a second terminal window on your Debian machine to test the /geo endpoint:
Query Google Public DNS:
curl -s 'http://localhost:8080/geo?ip=8.8.8.8' | jq
Sample Output:
{
"ip": "8.8.8.8",
"country_short": "US",
"country_long": "United States of America",
"region": "California",
"city": "Mountain View",
"isp": "Google LLC",
"latitude": 37.38605,
"longitude": -122.08385,
"domain": "google.com",
"zip_code": "94043",
"time_zone": "-07:00",
"net_speed": "T1",
"idd_code": "1",
"area_code": "650",
"weather_station_code": "USCA0746",
"weather_station_name": "Mountain View",
"mcc": "-",
"mnc": "-",
"mobile_brand": "-",
"elevation": 32,
"usage_type": "DCH",
"address_type": "A",
"category": "IAB19-11",
"district": "Santa Clara County",
"asn": "15169",
"as": "Google LLC",
"as_domain": "google.com",
"as_usage_type": "DCH",
"as_cidr": "8.8.8.0/24"
}

Test 2: Automated Testing with Random IP Addresses #
To verify stability across a continuous stream of distinct IP lookups, create a file called test10ips.sh and paste the codes below into it. Save the file.
# Function to generate a random public IPv4 address
random_ip() {
echo "$((RANDOM % 200 + 1)).$((RANDOM % 256)).$((RANDOM % 256)).$((RANDOM % 256))"
}
echo "Testing Go Geo API with 10 random IP addresses..."
echo "=================================================="
for i in {1..10}; do
TARGET_IP=$(random_ip)
# Query API and format response
RESULT=$(curl -s "http://localhost:8080/geo?ip=${TARGET_IP}")
COUNTRY=$(echo "$RESULT" | jq -r '.country_short // "N/A"')
ISP=$(echo "$RESULT" | jq -r '.isp // "N/A"')
USAGE=$(echo "$RESULT" | jq -r '.usage_type // "N/A"')
printf "IP: %-16s | Country: %-4s | Usage: %-5s | ISP: %s\n" "$TARGET_IP" "$COUNTRY" "$USAGE" "$ISP"
done
Now run the script to test 10 random IP addresses and see the results:
bash test10ips.sh

Test 3: High-Concurrency Load Testing #
To prove that ip2location-go easily manages hundreds of parallel requests against a single on-disk BIN file without lock contention, use a load-testing utility like hey:
Install hey load testing tool:
go install github.com/rakyll/hey@latest
Fire 20,000 requests using 100 concurrent workers:
~/go/bin/hey -n 20000 -c 100 http://localhost:8080/geo?ip=1.1.1.1

Conclusion #
Building a high-throughput geolocation API doesn’t require complex database clusters, heavy caching layers, or massive memory footprints. By leveraging Go’s native HTTP concurrency alongside ip2location-go, you can easily process over 13,000 requests per second using a single shared database instance.
Key takeaways from this setup:
- Memory Efficiency: Large datasets like the DB26 IPv6 BIN file stays safely on disk without exhausting server RAM, relying on OS-level page caching for fast lookups.
- Lock-Free Concurrency: Because the SDK uses Go’s stateless io.ReaderAt positional reads, hundreds of HTTP goroutines can query the database simultaneously without mutex lock contention.
- Low Single-Digit Millisecond Latency: Even under high concurrency (100 parallel connections), average response times stay around 7 ms with minimum latencies dipping down to 0.1 ms.
- Zero Dependencies: With zero external framework bloat, your API remains lightweight, fast, and easy to deploy across any architecture – from local edge devices to high-availability cloud instances.
By initializing ip2location.OpenDB() once at startup and passing the shared reference to your HTTP handlers, you get production-ready geolocation lookups with minimal overhead.
