func callRemoteService(ip string) ipInfoResult {
	var obj ipInfoResult
	api := "/json"
	if 0 == len(ip) {
		api = "json"
	}
	url := "https://ipinfo.io/" + ip + api
	resp, err := http.Get(url)
	if err != nil {
		fmt.Println("error: ", err)
		return obj
	}
	defer resp.Body.Close()
	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		fmt.Println("error: ", err)
		return obj
	}
	if strings.Contains(string(body), "Rate limit exceeded") {
		fmt.Println("\nError for:", url)
		fmt.Println(string(body))
		os.Exit(1)
	}
	json.Unmarshal(body, &obj)
	return obj
}
/*
callRemoteService issues a web query to ipinfo.io
The JSON result is converted to an ipInfoResult struct
Args:
ip: an IPv4 address

Returns:
an ipInfoResult struct containing the information returned by the service
package main
import (
	"encoding/json"
	"flag"
	"fmt"
	"io/ioutil"
	"math"
	"net"
	"net/http"
	"os"
	"regexp"
	"sort"
	"strconv"
	"strings"
	"time"
	"github.com/olekukonko/tablewriter"
)
const pgmVersion string = "1.1.3"
const pgmUrl string = "https://github.com/jftuga/ipinfo"
type dnsResponse struct {
	hostname  string
	addresses []string
	err       error
}
type ipInfoResult struct {
	Ip       string
	Hostname string
	City     string
	Region   string
	Country  string
	Loc      string
	Postal   string
	Org      string
	Distance float32
	ErrMsg   error
}
func main() {
	timeStart := time.Now()
	workers := flag.Int("t", 30, "number of simultaneous threads")
	tableAutoMerge := flag.Bool("m", false, "merge identical hosts")
	versionFlag := flag.Bool("v", false, "display program version and then exit")
	externalOnlyFlag := flag.Bool("x", false, "only display your external IP and then exit")
	wrapFlag := flag.Bool("w", false, "wrap output to better fit the screen width")
	flag.Parse()
	if *versionFlag {
		fmt.Println("version:", pgmVersion)
		fmt.Println(pgmUrl)
		return
	}
	localIpInfo := callRemoteService("")
	args := flag.Args()
	if *externalOnlyFlag {
		fmt.Println(localIpInfo.Ip)
		return
	}
	if len(flag.Args()) == 0 {
		args = append(args, localIpInfo.Ip)
	}
	convertedArgs := truncateArgParts(args)
	ipAddrs, reverseIP := runDNS(*workers, convertedArgs)
	ipInfo := resolveAllIpInfo(*workers, ipAddrs)
	outputTable(ipInfo, reverseIP, localIpInfo.Loc, *tableAutoMerge, *wrapFlag)
	elapsed := time.Since(timeStart)
	fmt.Println("\n")
	fmt.Printf("your IP addr : %v\n", localIpInfo.Ip)
	fmt.Printf("your location: %v\n", localIpInfo.Loc)
	fmt.Printf("elapsed time : %v\n", elapsed)
}
func truncateArgParts(rawArgs []string) []string {
	v4re := regexp.MustCompile(`(?:[0-9]{1,3}\.){3}[0-9]{1,3}`)
	truncateArgs := []string{}
	for entry := range rawArgs {
		if strings.Contains(rawArgs[entry], "://") {  
			slots := strings.SplitN(rawArgs[entry], "/", 4)
			truncateArgs = append(truncateArgs, slots[2])
			continue
		} else if strings.Contains(rawArgs[entry], "@") {  
			slots := strings.SplitN(rawArgs[entry], "@", 2)
			truncateArgs = append(truncateArgs, slots[1])
			continue
		} else {  
			if v4re.Match([]byte(rawArgs[entry])) && strings.Contains(rawArgs[entry],":") {
				c := strings.Index(rawArgs[entry],":")
				truncateArgs = append(truncateArgs, rawArgs[entry][0:c])
				continue
			}
			truncateArgs = append(truncateArgs, rawArgs[entry])
		}
	}
	return truncateArgs
}
func latlon2coord(latlon string) (float64, float64) {
	slots := strings.Split(latlon, ",")
	lat, err := strconv.ParseFloat(slots[0], 64)
	if err != nil {
		fmt.Println("Error converting latitude to float for:", latlon)
	}
	lon, err := strconv.ParseFloat(slots[1], 64)
	if err != nil {
		fmt.Println("Error converting longitude to float for:", latlon)
	}
	return lat, lon
}
func hsin(theta float64) float64 {
	return math.Pow(math.Sin(theta/2), 2)
}
func HaversineDistance(lat1, lon1, lat2, lon2 float64) float64 {
	var la1, lo1, la2, lo2, r float64
	piRad := math.Pi / 180
	la1 = lat1 * piRad
	lo1 = lon1 * piRad
	la2 = lat2 * piRad
	lo2 = lon2 * piRad
	r = 6378100  
	h := hsin(la2-la1) + math.Cos(la1)*math.Cos(la2)*hsin(lo2-lo1)
	meters := 2 * r * math.Asin(math.Sqrt(h))
	miles := meters / 1609.344
	return miles
}
func outputTable(ipInfo []ipInfoResult, reverseIP map[string]string, loc string, merge bool, wrap bool) {
	var allRows [][]string
	var distanceStr = ""
	for i, _ := range ipInfo {
		if strings.Contains(ipInfo[i].Ip, ":") {  
			continue
		}
		if ipInfo[i].Loc == "37.7510,-97.8220" || len(ipInfo[i].Loc) == 0 {  
			ipInfo[i].Loc = "N/A"
			ipInfo[i].City = "N/A"
			ipInfo[i].Region = "N/A"
			distanceStr = "N/A"
		} else {
			lat1, lon1 := latlon2coord(loc)
			lat2, lon2 := latlon2coord(ipInfo[i].Loc)
			miles := HaversineDistance(lat1, lon1, lat2, lon2)
			distanceStr = fmt.Sprintf("%.2f", miles)
		}
		row := []string{reverseIP[ipInfo[i].Ip], ipInfo[i].Ip, ipInfo[i].Hostname, ipInfo[i].Org, ipInfo[i].City, ipInfo[i].Region, ipInfo[i].Country, ipInfo[i].Loc, distanceStr}
		allRows = append(allRows, row)
	}
	sort.Slice(allRows, func(a, b int) bool {
		return allRows[a][0] < allRows[b][0]
	})
	table := tablewriter.NewWriter(os.Stdout)
	table.SetHeader([]string{"Input", "IP", "Hostname", "Org", "City", "Region", "Country", "Loc", "Distance"})
	if merge == true {
		table.SetAutoMergeCells(true)
	}
	if wrap {
		table.SetAutoWrapText(true)
	} else {
		table.SetAutoWrapText(false)
	}
	table.AppendBulk(allRows)
	table.Render()
}
func stringInSlice(a string, list []string) bool {
	for _, b := range list {
		if b == a {
			return true
		}
	}
	return false
}
func runDNS(workers int, hostnames []string) ([]string, map[string]string) {
	ipm, errors := resolveAllDNS(workers, hostnames)
	var ipAddrs []string
	ipAddrs = nil
	var reverseIP map[string]string
	reverseIP = make(map[string]string)
	for _, val := range ipm {
		for _, ip := range val.addresses {
			if stringInSlice(ip, ipAddrs) {  
				continue
			}
			ipAddrs = append(ipAddrs, ip)
			reverseIP[ip] = val.hostname
		}
	}
	if len(errors) > 0 {
		var errBuilder strings.Builder
		for _, err := range errors {
			errBuilder.WriteString(fmt.Sprintf("%s\n", err.Error()))
		}
		fmt.Printf("\n%s\n\n", errBuilder.String())
	}
	return ipAddrs, reverseIP
}
func resolveAllDNS(workers int, hostnames []string) ([]dnsResponse, []error) {
	workCh := make(chan string)
	dnsResponseCh := make(chan dnsResponse)
	defer close(dnsResponseCh)
	for i := 0; i < workers; i++ {
		go workDNS(workCh, dnsResponseCh)
	}
	allDnsReplies := []dnsResponse{}
	waitingFor := 0
	errors := []error{}
	for len(hostnames) > 0 || waitingFor > 0 {
		sendCh := workCh
		host := ""
		if len(hostnames) > 0 {
			host = hostnames[0]
		} else {
			sendCh = nil
		}
		select {
		case sendCh <- host:
			waitingFor++
			hostnames = hostnames[1:]
		case dnsResponse := <-dnsResponseCh:
			waitingFor--
			if dnsResponse.err != nil {
				errors = append(errors, dnsResponse.err)
			} else {
				allDnsReplies = append(allDnsReplies, dnsResponse)
			}
		}
	}
	return allDnsReplies, errors
}
func workDNS(workCh chan string, dnsResponseCh chan dnsResponse) {
	for hostname := range workCh {
		addresses, err := net.LookupHost(hostname)
		dnsResponseCh <- dnsResponse{
			hostname:  hostname,
			addresses: addresses,
			err:       err,
		}
	}
}
func resolveAllIpInfo(workers int, ipAddrs []string) []ipInfoResult {
	workCh := make(chan string)
	resultsCh := make(chan ipInfoResult)
	defer close(resultsCh)
	for i := 0; i < workers; i++ {
		go workIpInfoLookup(workCh, resultsCh)
	}
	var iir []ipInfoResult
	waitingFor := 0
	for len(ipAddrs) > 0 || waitingFor > 0 {
		sendCh := workCh
		ip := ""
		if len(ipAddrs) > 0 {
			ip = ipAddrs[0]
		} else {
			sendCh = nil
		}
		select {
		case sendCh <- ip:
			waitingFor++
			ipAddrs = ipAddrs[1:]
		case result := <-resultsCh:
			waitingFor--
			iir = append(iir, result)
		}
	}
	return iir
}