Golang portping: TCP checks with Ping and PingN (sequential, parallel, ranges)

Golang portping with janosgyerik/portping: set up module, Ping and PingN API, sequential and parallel TCP checks, port ranges, PingN result handling, retries, per-dial timing, CLI install, common dial errors; links to goroutines, channels, net, and shell checks.

Published

Updated

Read time 8 min read

Reviewed byDeepak Prasad

Golang portping: TCP checks with Ping and PingN (sequential, parallel, ranges)

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.1 and 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.

bash
mkdir -p ~/projects/portpingdemo
cd ~/projects/portpingdemo
go mod init portpingdemo
go get github.com/janosgyerik/portping@v1.0.1

Example tool output:

text
go: creating new go.mod: module portpingdemo
go: added github.com/janosgyerik/portping v1.0.1

Pin 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):

bash
go install github.com/janosgyerik/portping/cmd/portping@v1.0.1

Then 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):

text
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.

go
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:

text
Port 443 on host one.one.one.one is accessible.
[attempt 1] ok
[attempt 2] ok
[attempt 3] ok

Advanced 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.

go
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):

text
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 timeout

2. 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).

go
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):

text
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.

go
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:

text
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 refused

4. Handling results from PingN

Count successes, collect failures, or compute a simple success ratio. Always size the channel for the number of results.

go
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.

go
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).

go
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:

text
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.


References


Frequently Asked Questions

1. What is portping in Go?

portping is a small third-party package (github.com/janosgyerik/portping) that tries to open a TCP connection with net.DialTimeout, then closes it. A nil error means the remote accepted the dial on that port within the timeout you chose.

2. Does portping send an ICMP ping?

No. It uses TCP (or whatever network you pass, typically tcp) like a miniature connect check, not ICMP echo.

3. What is the difference between Ping and PingN?

Ping tries once and returns an error. PingN repeats the same dial count times and sends each result on a channel you provide. PingN runs in the caller goroutine and blocks until every result is sent, so the channel must be buffered with capacity at least count, or another goroutine must receive while PingN runs.

4. Why does my program hang when I use PingN?

An unbuffered channel combined with receiving only after PingN returns can deadlock: PingN blocks on the first send until a receiver runs, but the receiver is in the same goroutine after PingN. Use make(chan error, count) or receive concurrently.

5. Can I use portping to scan the whole internet?

You should only probe hosts and ports you own or are explicitly authorized to test. Bulk or hostile scanning can violate provider terms and local law; treat this library as an operational helper, not a weapon.
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive …