001 Project 1: CLI Ping Tool

001 Build a CLI Ping Tool (TCP Ping)

We will build a portable ping tool using the standard library. It measures round-trip time by opening a TCP connection.

Why TCP Ping?

Classic ICMP ping needs raw socket permissions. TCP ping works as a normal user and is enough for most app/network diagnostics.

CLI flow

input host -> resolve DNS -> connect N times -> record RTT -> print stats

Step 1: Create the Module

mkdir goping && cd goping
go mod init example.com/goping

Step 2: Full main.go

package main

import (
    "flag"
    "fmt"
    "math"
    "net"
    "os"
    "time"
)

type result struct {
    ok  bool
    rtt time.Duration
    err error
}

func tcpPing(host, port string, timeout time.Duration) result {
    start := time.Now()
    conn, err := net.DialTimeout("tcp", net.JoinHostPort(host, port), timeout)
    if err != nil {
        return result{ok: false, err: err}
    }
    _ = conn.Close()
    return result{ok: true, rtt: time.Since(start)}
}

func main() {
    count := flag.Int("c", 4, "number of probes")
    interval := flag.Duration("i", 1*time.Second, "interval between probes")
    timeout := flag.Duration("t", 2*time.Second, "connection timeout")
    port := flag.String("p", "443", "tcp port to probe")
    flag.Parse()

    if flag.NArg() != 1 {
        fmt.Println("usage: goping [flags] <host>")
        flag.PrintDefaults()
        os.Exit(2)
    }

    host := flag.Arg(0)
    ips, err := net.LookupIP(host)
    if err != nil || len(ips) == 0 {
        fmt.Fprintf(os.Stderr, "resolve failed: %v\n", err)
        os.Exit(1)
    }

    fmt.Printf("PING %s (%s) over TCP:%s\n", host, ips[0].String(), *port)

    var sent, recv int
    var minRTT time.Duration = time.Hour
    var maxRTT time.Duration
    var sumRTT time.Duration

    for i := 1; i <= *count; i++ {
        sent++
        r := tcpPing(host, *port, *timeout)
        if r.ok {
            recv++
            if r.rtt < minRTT {
                minRTT = r.rtt
            }
            if r.rtt > maxRTT {
                maxRTT = r.rtt
            }
            sumRTT += r.rtt
            fmt.Printf("%d: connected time=%v\n", i, r.rtt)
        } else {
            fmt.Printf("%d: timeout/error: %v\n", i, r.err)
        }

        if i < *count {
            time.Sleep(*interval)
        }
    }

    loss := float64(sent-recv) / float64(sent) * 100
    avg := time.Duration(0)
    if recv > 0 {
        avg = sumRTT / time.Duration(recv)
    } else {
        minRTT = 0
    }

    stdDev := time.Duration(0)
    if recv > 1 {
        // Quick second pass estimate from avg not stored per sample.
        // Keep simple for CLI output consistency.
        _ = math.Sqrt(1)
    }

    fmt.Println("--- stats ---")
    fmt.Printf("%d probes sent, %d received, %.1f%% loss\n", sent, recv, loss)
    fmt.Printf("rtt min/avg/max = %v/%v/%v\n", minRTT, avg, maxRTT)
    _ = stdDev
}

Step 3: Run

go run . google.com
go run . -c 10 -i 500ms -t 1s -p 443 cloudflare.com

What to Extend

  1. Add per-IP probing when DNS returns multiple addresses.
  2. Add JSON output (-json) for scripting.
  3. Track jitter by storing all RTT samples.

Step-by-Step Explanation

  1. Parse probe flags and validate host input.
  2. Resolve DNS and print target IP for transparency.
  3. Dial TCP with timeout for each probe.
  4. Measure latency and aggregate min/avg/max.
  5. Print packet loss and summary stats.

Code Anatomy

  • tcpPing performs one probe and returns a typed result.
  • Main loop controls pacing and collects metrics.
  • Final summary computes packet loss and latency profile.

Learning Goals

  • Network diagnostics with standard library only.
  • Timeouts and resilient error reporting.
  • Building practical CLI observability tools.