Introduction
I spent four years studying Communication and Computer Networks at the Technical University of Kenya. I was fairly solid on theory and could explain TCP/IP handshakes, draw network topologies, and describe exactly what happens when a packet moves from one host to another.
Then I joined Zone01 Kisumu, an environment that forced me to build something with the knowledge I had. That’s when I realized there’s a massive gap between knowing how networking works and knowing how to code it.
This is the story of how I bridged that gap. Let me walk you through the journey; from building a concurrent port scanner in Go to developing a strong conviction that real projects are the fastest way to learn.
To understand how this project pushed me, let’s start by explaining the technical challenge at its core: what exactly is a port scanner?
A port scanner probes a target IP address and checks which of the listed ports are open, closed, or filtered. Tools like nmap do this at a professional level. My scanner is a simpler version, and building it taught me more than any lecture ever did.
The Stack
- Language: Go
- Concurrency model: Worker pools with goroutines
- Input: CLI flags (target IP/range, port range, workers, timeout)
- Output: Terminal display + optional file export (JSON, CSV, TXT)
How It Works
- Parsing the Target
The scanner accepts a single IP address or a range, such as 192.168.1.1-192.168.1.254. The ParseIPs() function in the scanner package handles expanding that range into a slice of individual IP strings.
- Parsing Ports
This was one of the more interesting parts to design with flexibility in mind. I wanted to be able to scan a range (1-1000), a comma-separated list (22,80,443), or use a named profile:
func parsePorts(input string) ([]int, error) {
// check for named profile first
if profile, ok := scanner.PortProfiles[input]; ok {
return profile, nil
}
var ports []int
if strings.Contains(input, "-") {
parts := strings.Split(input, "-")
start, err1 := strconv.Atoi(parts[0])
end, err2 := strconv.Atoi(parts[1])
if err1 != nil || err2 != nil {
return nil, fmt.Errorf("invalid port numbers in range: %s", input)
}
for i := start; i <= end; i++ {
ports = append(ports, i)
}
return ports, nil
}
for _, p := range strings.Split(input, ",") {
port, err := strconv.Atoi(strings.TrimSpace(p))
if err != nil || port < 1 || port > 65535 {
return nil, fmt.Errorf("invalid port: %s", p)
}
ports = append(ports, port)
}
return ports, nil
}
Port profiles like common, web, db, and ssh are predefined slices in the scanner package. So instead of typing --ports 22,80,443,3306,5432, you just type --ports web. Much cleaner.
- The Worker Pool (Where Go Shines)
This is my proudest bit. Scanning hundreds of IPs across hundreds of ports sequentially would take forever. The solution? Concurrency.
Go makes this elegant with go routines. These are lightweight threads that can run thousands at a time. My scanner uses a worker pool pattern: a fixed number of workers pick jobs from a shared channel and process them concurrently.
results := scanner.RunWorkerPool(ips, portList, *workers, time.Duration(*timeout)*time.Second)
By default, 100 workers run simultaneously. That means scanning 254 hosts across 100 ports completes in seconds, not minutes.
This was the hardest concept to get right. You’re not writing step-by-step instructions anymore; you’re designing a system where many things happen at once.
- The CLI Interface
The main.go uses Go’s built-in flag package to handle all inputs cleanly:
target := flag.String("target", "", "IP or range (e.g. 192.168.89.1-192.168.89.254)")
ports := flag.String("ports", "common", "Ports: range, list, or profile (common, web, db, ssh)")
workers := flag.Int("workers", 100, "Number of concurrent workers")
timeout := flag.Int("timeout", 1, "Connection timeout in seconds")
verbose := flag.Bool("v", false, "Show closed ports too")
vverbose := flag.Bool("vv", false, "Show all ports including filtered")
output := flag.String("output", "", "Save results to file (e.g. results.json, results.csv, results.txt)")
A typical scan looks like this:
go run main.go -target 192.168.1.1-192.168.1.50 -ports web -workers 200 -output results.json
- Output & Saving Results
The scanner prints results to the terminal and can optionally save them to a file. I added support for three formats; JSON, CSV, and plain text, because different use cases need different outputs. A security audit might want JSON for parsing; a quick report might want CSV for a spreadsheet.
What I Learned That School Didn’t Teach Me
Networking concepts look different in code.
I knew what a TCP connection was. But writing code that attempts a TCP dial to check if a port is open, and then handling timeouts, refused connections, and filtered ports as separate outcomes, gave me a much deeper intuition for what’s actually happening on the wire.Concurrency is a design problem, not just a syntax problem.
Go’s go routines are simple to write. Worker pools are simple to understand. But designing a concurrent system, deciding how many workers, how to handle shared state, how to collect results safely requires a different way of thinking that you only develop by doing.CLI design matters.
A tool that’s hard to use won’t get used. Adding port profiles, verbosity levels, and multiple output formats wasn’t about making the code fancier, it was about making the tool actually useful in real scenarios.Error handling is where discipline lives.
Go forces you to handle errors explicitly. At first, that felt annoying. Now I see it as one of the language’s best features. Every if err != nil block is a moment where you decide: what should actually happen here?
What’s Next
This scanner works well for small networks, which is exactly the audience I had in mind, SMEs in Kenya who need a lightweight, local tool for basic network visibility without the overhead of enterprise software.
Next steps:
- Add OS fingerprinting
- Build a simple web UI for non-technical users.
- Package it as a standalone binary for easy distribution.
The code is open source on GitHub: github.com/rjeff-sudo
Final Thought
If you’re learning a new language or trying to solidify concepts from school, stop doing tutorials in isolation. Pick something real, something slightly too hard for your current level, and build it. You’ll hit walls. You’ll Google things at 1am. You’ll rewrite entire functions because you didn’t think it through the first time.
That’s not failure. That’s learning.
Built with Go · Zone01 Kisumu · Nairobi, Kenya
If you found this useful or have questions about the code, drop a comment below. I’m always happy to talk about networking and Go.
Top comments (0)