When you automate health checks, load balancers, or firewall rules, you often need to know whether a TCP port on a host accepts connections. The portping module by Janos Gyerik wraps net.DialTimeout so a successful dial (then immediate close) means “port reachable” without shelling out to nc or telnet. The same repository ships a small CLI under cmd/portping. This guide follows a practical layout: environment setup, API parameters, Ping / PingN, basic usage, then advanced patterns (sequential, parallel, port ranges, aggregating PingN results, retries, timing) and common errors. For shell-first checks, see test port connectivity. For concurrency primitives, see goroutines and channels.
Catchy marketing titles can work for social share, but for technical SEO a clear page title plus a keyword-oriented seoTitle (as in the front matter above) is usually the better default: search snippets still pull title heavily, so the title should read like a real topic, not clickbait alone.
Examples below use
github.com/janosgyerik/portping@v1.0.1and Go 1.24 on 64-bit Linux. Network-dependent snippets use{run=false}; sample output was captured from real runs on that environment.
Set up environment
You need the Go toolchain, network egress to the hosts you probe, and a module for your program.
mkdir -p ~/projects/portpingdemo
cd ~/projects/portpingdemo
go mod init portpingdemo
go get github.com/janosgyerik/portping@v1.0.1Example tool output:
go: creating new go.mod: module portpingdemo
go: added github.com/janosgyerik/portping v1.0.1Pin the version in go.mod for reproducible CI checks. Add main.go beside go.mod for the snippets in this article.
Optional: install the CLI
The repo provides cmd/portping (see upstream README for flags such as -c count and -W wait seconds):
go install github.com/janosgyerik/portping/cmd/portping@v1.0.1Then portping -h shows usage in the form portping [options] host port (for example portping -c 3 one.one.one.one 443).
Syntax and parameters of portping
The library exposes two functions (see portping.go on the v1.0.1 tag):
func Ping(network, address string, timeout time.Duration) error
func PingN(network, address string, timeout time.Duration, count int, c chan<- error)| Parameter | Role |
|---|---|
network |
First argument to net.DialTimeout, usually "tcp". |
address |
Host and port in dial form, for example "db.internal:5432" or "10.0.0.5:22". |
timeout |
Per-attempt dial budget; use time.Duration (for example 3 * time.Second). |
count |
PingN only: how many sequential Ping calls to perform. |
c |
PingN only: channel receives one error per attempt (nil means success). |
Return values are normal Go error values: nil means the TCP handshake completed within the timeout (then the connection is closed in Ping).
Ping function
Ping performs exactly one dial. On success it closes the connection before returning nil. On failure it returns the error from net.DialTimeout without an extra wrapper type, so you can use errors.Is with timeouts where applicable.
PingN function
PingN is a thin loop around Ping: it does not spawn its own goroutine for the loop body. Each result is sent on c in order; the implementation is only a few lines in the upstream source, which makes behavior easy to audit when you design channel buffering.
Basic usage of portping
Single host and port: try Ping, then repeat with PingN and a buffered channel.
package main
import (
"fmt"
"time"
"github.com/janosgyerik/portping"
)
func main() {
host := "one.one.one.one"
port := "443"
address := host + ":" + port
timeout := 5 * time.Second
if err := portping.Ping("tcp", address, timeout); err == nil {
fmt.Printf("Port %s on host %s is accessible.\n", port, host)
} else {
fmt.Printf("Port %s on host %s failed: %v\n", port, host, err)
}
const n = 3
ch := make(chan error, n)
portping.PingN("tcp", address, timeout, n, ch)
for i := 0; i < n; i++ {
if err := <-ch; err == nil {
fmt.Printf("[attempt %d] ok\n", i+1)
} else {
fmt.Printf("[attempt %d] %v\n", i+1, err)
}
}
}Sample run:
Port 443 on host one.one.one.one is accessible.
[attempt 1] ok
[attempt 2] ok
[attempt 3] okAdvanced usage of portping
1. Checking connectivity of multiple ports sequentially
A for loop keeps ordering obvious and avoids goroutine noise when the list is short.
package main
import (
"fmt"
"time"
"github.com/janosgyerik/portping"
)
func main() {
host := "one.one.one.one"
ports := []string{"443", "80", "65535"}
timeout := 3 * time.Second
for _, port := range ports {
address := host + ":" + port
if err := portping.Ping("tcp", address, timeout); err == nil {
fmt.Printf("Port %s on host %s is accessible.\n", port, host)
} else {
fmt.Printf("Port %s on host %s is not accessible. Error: %s\n", port, host, err)
}
}
}Sample run (resolved IP may vary with DNS):
Port 443 on host one.one.one.one is accessible.
Port 80 on host one.one.one.one is accessible.
Port 65535 on host one.one.one.one is not accessible. Error: dial tcp 1.1.1.1:65535: i/o timeout2. Checking connectivity of multiple ports in parallel
Use sync.WaitGroup so main waits for every probe. Pass the port into the goroutine closure argument so the body does not capture a loop variable by mistake (Go 1.22+ fixed the classic loop-var capture bug, but the go func(p int) { ... }(port) style stays explicit).
package main
import (
"fmt"
"sync"
"time"
"github.com/janosgyerik/portping"
)
func main() {
host := "one.one.one.one"
ports := []int{443, 80, 65535}
timeout := 3 * time.Second
var wg sync.WaitGroup
for _, port := range ports {
wg.Add(1)
go func(p int) {
defer wg.Done()
address := fmt.Sprintf("%s:%d", host, p)
if err := portping.Ping("tcp", address, timeout); err == nil {
fmt.Printf("Port %d is accessible.\n", p)
} else {
fmt.Printf("Error with port %d: %s\n", p, err)
}
}(port)
}
wg.Wait()
}Sample run (lines may appear in any order):
Error with port 65535: dial tcp 1.1.1.1:65535: i/o timeout
Port 443 is accessible.
Port 80 is accessible.3. Checking the connectivity of a range of ports
Loop numeric bounds when you care about a slice of consecutive ports (for example a local ephemeral range). Use a short timeout on closed ports so localhost “connection refused” returns quickly; wide ranges with long timeouts still take wall-clock time.
package main
import (
"fmt"
"time"
"github.com/janosgyerik/portping"
)
func main() {
host := "127.0.0.1"
timeout := 300 * time.Millisecond
for p := 7999; p <= 8003; p++ {
addr := fmt.Sprintf("%s:%d", host, p)
err := portping.Ping("tcp", addr, timeout)
fmt.Printf("%s -> %v\n", addr, err)
}
}Sample run on a quiet workstation:
127.0.0.1:7999 -> dial tcp 127.0.0.1:7999: connect: connection refused
127.0.0.1:8000 -> dial tcp 127.0.0.1:8000: connect: connection refused
127.0.0.1:8001 -> dial tcp 127.0.0.1:8001: connect: connection refused
127.0.0.1:8002 -> dial tcp 127.0.0.1:8002: connect: connection refused
127.0.0.1:8003 -> dial tcp 127.0.0.1:8003: connect: connection refused4. Handling results from PingN
Count successes, collect failures, or compute a simple success ratio. Always size the channel for the number of results.
count := 5
ch := make(chan error, count)
portping.PingN("tcp", "one.one.one.one:443", 2*time.Second, count, ch)
ok := 0
var fails []error
for i := 0; i < count; i++ {
if err := <-ch; err == nil {
ok++
} else {
fails = append(fails, err)
}
}
fmt.Printf("%d of %d dials succeeded; failures=%d\n", ok, count, len(fails))PingN does not attach attempt indices to errors; if you need per-attempt metadata (index, timestamp), wrap Ping in your own loop instead of PingN.
5. Retrying failed pings
PingN is a burst of identical attempts, not exponential backoff. For “retry until success or cap,” wrap Ping yourself with time.Sleep between tries, or combine PingN with a threshold on the success count.
package main
import (
"time"
"github.com/janosgyerik/portping"
)
func pingWithRetry(network, address string, timeout time.Duration, tries int, pause time.Duration) error {
var last error
for i := 0; i < tries; i++ {
if i > 0 {
time.Sleep(pause)
}
last = portping.Ping(network, address, timeout)
if last == nil {
return nil
}
}
return last
}6. Logging time taken for each ping
Ping/PingN do not return latency; record time.Now() before and after each Ping when you need RTT-style logging (rough TCP connect time, not ICMP).
package main
import (
"fmt"
"time"
"github.com/janosgyerik/portping"
)
func main() {
addr := "one.one.one.one:443"
timeout := 2 * time.Second
for attempt := 1; attempt <= 3; attempt++ {
t0 := time.Now()
err := portping.Ping("tcp", addr, timeout)
fmt.Printf("attempt %d took %v err=%v\n", attempt, time.Since(t0).Round(time.Millisecond), err)
}
}Sample run:
attempt 1 took 113ms err=<nil>
attempt 2 took 99ms err=<nil>
attempt 3 took 59ms err=<nil>Common errors
Connection refused
The error text often contains connect: connection refused. The host routed the packet, but nothing accepted the TCP connection on that port (or a firewall rejected it sharply). Confirm the process listen address (0.0.0.0 vs 127.0.0.1) and local firewall rules.
Timeouts (i/o timeout)
The handshake did not finish before your deadline. Typical causes are filters, wrong IP, or the service only listening elsewhere. Increase the timeout only after the path should be fast; otherwise fix routing or security groups first.
DNS failures
Strings like no such host mean the name did not resolve. Check resolver configuration, VPN, or typos. Dialing a literal IP bypasses DNS for a control test.
No route to host
Usually routing or an intermediate drop. Verify interface metrics, VPN routes, and cloud network ACLs. On Linux hosts, ip route is the usual next step outside Go.
Deadlock with PingN and channels
If PingN blocks forever, re-read PingN function: unbuffered channel plus receive-after-PingN is the usual mistake.
Summary
portping offers a minimal API on top of the standard net stack: Ping for one-off TCP reachability, PingN when you want several sequential samples on a channel (with buffering or concurrent receive), plus patterns for sequential lists, parallel probes with WaitGroup, numeric port ranges, retries, and per-attempt timing you implement yourself. Read dial errors literally—refused, timeout, and DNS each imply different fixes. Use the CLI for ad hoc checks and the library inside services; only probe infrastructure you are allowed to test.

