Getting the public IP in Go — no dependencies, no API key
Go's standard library is famously self-contained, and IP detection is a case where that pays off directly. Everything in this article uses only the standard library plus one optional third-party HTTP client — no SDK, no configuration file, no API key required. The API is IPPubblico.org: free, HTTPS, plain text and JSON endpoints, no authentication.
Use case 1 — Your server's own public IP (one-liner)
The simplest case: a Go program or script that needs to know its own public IP.
package main
import (
"fmt"
"io"
"net/http"
"strings"
"time"
)
func getPublicIP() (string, error) {
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Get("https://ipv4.ippubblico.org/")
if err != nil {
return "", err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
return strings.TrimSpace(string(body)), nil
}
func main() {
ip, err := getPublicIP()
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
fmt.Printf("Public IP: %s\n", ip) // Public IP: 203.0.113.42
}
No imports beyond the standard library. The Timeout on the HTTP client prevents the program from hanging indefinitely if the network is unavailable.
Use case 2 — Full geolocation data
When you need country, city, ISP and ASN in addition to the IP address:
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"time"
)
type GeoData struct {
Country string `json:"country"`
CountryCode string `json:"country_code"`
City string `json:"city"`
Region string `json:"region"`
Lat float64 `json:"lat"`
Lon float64 `json:"lon"`
}
type IPInfo struct {
Status string `json:"status"`
IP string `json:"ip"`
IPv4 string `json:"ipv4"`
IPv6 *string `json:"ipv6"`
ISP string `json:"isp"`
ASN string `json:"asn"`
Timezone string `json:"timezone"`
Geo GeoData `json:"geo"`
}
func getIPInfo() (*IPInfo, error) {
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Get("https://ippubblico.org/?api=1")
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var info IPInfo
if err := json.Unmarshal(body, &info); err != nil {
return nil, err
}
if info.Status != "ok" {
return nil, fmt.Errorf("API returned status: %s", info.Status)
}
return &info, nil
}
func main() {
info, err := getIPInfo()
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
fmt.Printf("IP: %s\n", info.IP)
fmt.Printf("Country: %s (%s)\n", info.Geo.Country, info.Geo.CountryCode)
fmt.Printf("City: %s, %s\n", info.Geo.City, info.Geo.Region)
fmt.Printf("ISP: %s\n", info.ISP)
fmt.Printf("Timezone: %s\n", info.Timezone)
}
Use case 3 — Reusable client with caching
In a real application you rarely want to call the API on every request. This client caches the result for a configurable duration:
package ipdetect
import (
"encoding/json"
"fmt"
"io"
"net/http"
"sync"
"time"
)
type Client struct {
httpClient *http.Client
cacheTTL time.Duration
mu sync.Mutex
cached *IPInfo
cachedAt time.Time
}
type IPInfo struct {
IP string `json:"ip"`
Country string `json:"country"`
City string `json:"city"`
ISP string `json:"isp"`
ASN string `json:"asn"`
}
func NewClient(cacheTTL time.Duration) *Client {
return &Client{
httpClient: &http.Client{Timeout: 10 * time.Second},
cacheTTL: cacheTTL,
}
}
func (c *Client) Get() (*IPInfo, error) {
c.mu.Lock()
defer c.mu.Unlock()
if c.cached != nil && time.Since(c.cachedAt) < c.cacheTTL {
return c.cached, nil
}
resp, err := c.httpClient.Get("https://ippubblico.org/?api=1")
if err != nil {
return nil, fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("read body: %w", err)
}
var raw struct {
Status string `json:"status"`
IP string `json:"ip"`
ISP string `json:"isp"`
ASN string `json:"asn"`
Geo struct {
Country string `json:"country"`
City string `json:"city"`
} `json:"geo"`
}
if err := json.Unmarshal(body, &raw); err != nil {
return nil, fmt.Errorf("parse JSON: %w", err)
}
info := &IPInfo{
IP: raw.IP,
ISP: raw.ISP,
ASN: raw.ASN,
Country: raw.Geo.Country,
City: raw.Geo.City,
}
c.cached = info
c.cachedAt = time.Now()
return info, nil
}
Usage:
client := ipdetect.NewClient(1 * time.Hour)
info, err := client.Get() // calls API
info, err = client.Get() // returns cached result
Use case 4 — DDNS updater
A production-ready DDNS updater that detects IP changes and updates a Cloudflare DNS record:
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"strings"
"time"
)
const (
cfAPIToken = "your_cloudflare_api_token"
cfZoneID = "your_zone_id"
cfRecordID = "your_record_id"
recordName = "home.yourdomain.com"
cacheFile = "/tmp/ddns_last_ip.txt"
checkEvery = 5 * time.Minute
)
var httpClient = &http.Client{Timeout: 10 * time.Second}
func getPublicIP() (string, error) {
resp, err := httpClient.Get("https://ipv4.ippubblico.org/")
if err != nil {
return "", err
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
return strings.TrimSpace(string(body)), nil
}
func getLastKnownIP() string {
data, err := os.ReadFile(cacheFile)
if err != nil {
return ""
}
return strings.TrimSpace(string(data))
}
func saveIP(ip string) {
os.WriteFile(cacheFile, []byte(ip), 0644)
}
func updateCloudflare(ip string) error {
url := fmt.Sprintf(
"https://api.cloudflare.com/client/v4/zones/%s/dns_records/%s",
cfZoneID, cfRecordID,
)
payload := map[string]interface{}{
"type": "A",
"name": recordName,
"content": ip,
"ttl": 60,
}
body, _ := json.Marshal(payload)
req, _ := http.NewRequest("PUT", url, bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+cfAPIToken)
req.Header.Set("Content-Type", "application/json")
resp, err := httpClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
var result map[string]interface{}
json.NewDecoder(resp.Body).Decode(&result)
if result["success"] != true {
return fmt.Errorf("cloudflare error: %v", result["errors"])
}
return nil
}
func checkAndUpdate() {
current, err := getPublicIP()
if err != nil {
log.Printf("Failed to get IP: %v", err)
return
}
last := getLastKnownIP()
if current == last {
log.Printf("IP unchanged: %s", current)
return
}
log.Printf("IP changed: %s → %s", last, current)
if err := updateCloudflare(current); err != nil {
log.Printf("DNS update failed: %v", err)
return
}
saveIP(current)
log.Printf("DNS updated to %s", current)
}
func main() {
log.Println("DDNS updater started")
checkAndUpdate()
for range time.Tick(checkEvery) {
checkAndUpdate()
}
}
Build as a single static binary — no runtime dependencies:
go build -o ddns_updater .
# or cross-compile for Linux ARM (e.g. Raspberry Pi)
GOOS=linux GOARCH=arm64 go build -o ddns_updater_arm64 .
Use case 5 — HTTP middleware for Gin
Detect the client's country and attach it to the Gin context for use in any handler:
package middleware
import (
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"sync"
"time"
"github.com/gin-gonic/gin"
)
var (
geoCache = make(map[string]string)
geoCacheMu sync.RWMutex
httpClient = &http.Client{Timeout: 3 * time.Second}
)
func getCountry(ip string) string {
geoCacheMu.RLock()
if c, ok := geoCache[ip]; ok {
geoCacheMu.RUnlock()
return c
}
geoCacheMu.RUnlock()
resp, err := httpClient.Get("https://ippubblico.org/?api=1")
if err != nil {
return ""
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
var data struct {
Geo struct {
CountryCode string `json:"country_code"`
} `json:"geo"`
}
json.Unmarshal(body, &data)
geoCacheMu.Lock()
geoCache[ip] = data.Geo.CountryCode
geoCacheMu.Unlock()
return data.Geo.CountryCode
}
func GeoMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
ip := c.ClientIP()
country := getCountry(ip)
c.Set("country_code", country)
c.Next()
}
}
Usage in any Gin handler:
r := gin.Default()
r.Use(middleware.GeoMiddleware())
r.GET("/content", func(c *gin.Context) {
country, _ := c.Get("country_code")
c.JSON(200, gin.H{
"country": country,
"content": "Hello from " + fmt.Sprint(country),
})
})
Use case 6 — IPv4 and IPv6 detection
package main
import (
"bufio"
"fmt"
"net/http"
"strings"
"time"
)
type DualStackIP struct {
IPv4 string
IPv6 string
}
func getBothIPs() (*DualStackIP, error) {
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Get("https://ippubblico.org/?text=1")
if err != nil {
return nil, err
}
defer resp.Body.Close()
result := &DualStackIP{}
scanner := bufio.NewScanner(resp.Body)
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(line, "IPv4: ") {
v := strings.TrimPrefix(line, "IPv4: ")
if v != "NONE" {
result.IPv4 = v
}
} else if strings.HasPrefix(line, "IPv6: ") {
v := strings.TrimPrefix(line, "IPv6: ")
if v != "NONE" {
result.IPv6 = v
}
}
}
return result, scanner.Err()
}
func main() {
ips, err := getBothIPs()
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
fmt.Printf("IPv4: %s\n", ips.IPv4) // IPv4: 203.0.113.42
fmt.Printf("IPv6: %s\n", ips.IPv6) // IPv6: (empty if unavailable)
}
Handling rate limits
The API returns 429 Too Many Requests with a Retry-After header if the same IP sends requests too frequently. A robust wrapper with retry logic:
func getPublicIPWithRetry(maxRetries int) (string, error) {
client := &http.Client{Timeout: 10 * time.Second}
for attempt := 0; attempt <= maxRetries; attempt++ {
resp, err := client.Get("https://ipv4.ippubblico.org/")
if err != nil {
return "", err
}
if resp.StatusCode == http.StatusTooManyRequests {
retryAfter := resp.Header.Get("Retry-After")
wait := 20 * time.Second
if secs, err := time.ParseDuration(retryAfter + "s"); err == nil {
wait = secs
}
resp.Body.Close()
if attempt < maxRetries {
time.Sleep(wait)
continue
}
return "", fmt.Errorf("rate limited after %d attempts", maxRetries)
}
body, err := io.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
return "", err
}
return strings.TrimSpace(string(body)), nil
}
return "", fmt.Errorf("max retries exceeded")
}
Quick reference
| Need | Endpoint | Response |
|---|---|---|
| IPv4 only | https://ipv4.ippubblico.org/ |
203.0.113.42 |
| IPv6 only | https://ipv6.ippubblico.org/ |
2001:db8::1 or NONE
|
| Both protocols | https://ippubblico.org/?text=1 |
IPv4: x\nIPv6: x |
| Full geolocation | https://ippubblico.org/?api=1 |
JSON with country, city, ISP |
Full API documentation: ippubblico.org/docs.html
Conclusion
Go's standard library handles all the HTTP and JSON work cleanly — the code you write for IP detection in Go is shorter and more explicit than most other languages, with no hidden magic. The DDNS updater compiles to a single static binary that you can drop on any Linux server or embedded device without installing a runtime.
The caching client and Gin middleware patterns shown here are production-ready starting points — they handle concurrent access correctly with sync.RWMutex and fail gracefully when the API is unavailable.
Building something with Go and IPPubblico? Share your use case in the comments.
Top comments (0)