Backend Development 8 min read

Building a Simple TCP Port Scanner in Go

This article walks through creating a lightweight TCP port scanner in Go, covering basic TCP handshake theory, single‑port testing, looping over ports, adding concurrency with WaitGroup, implementing timeouts, using the flag package for command‑line options, and handling race conditions with a mutex.

360 Tech Engineering
360 Tech Engineering
360 Tech Engineering
Building a Simple TCP Port Scanner in Go

Go is well‑suited for network application programming, offering a powerful standard library that simplifies tasks such as building a TCP scanner.

The tutorial begins with a brief overview of the TCP three‑way handshake, explaining how a SYN packet initiates a connection and how a SYN‑ACK response indicates an open port, while an RST response signals a closed one.

First, a simple single‑port test is shown using net.Dial to attempt a connection to a host and port, printing whether the connection succeeded.

package main

import (
    "fmt"
    "net"
)

func main() {
    _, err := net.Dial("tcp", "google.com:80")
    if err == nil {
        fmt.Println("Connection successful")
    } else {
        fmt.Println(err)
    }
}

To scan a range of ports, a loop is added that iterates from port 80 to 99, attempting a connection for each and reporting success or failure.

package main

import (
    "fmt"
    "net"
)

func main() {
    for port := 80; port < 100; port++ {
        conn, err := net.Dial("tcp", fmt.Sprintf("google.com:%d", port))
        if err == nil {
            conn.Close()
            fmt.Println("Connection successful")
        } else {
            fmt.Println(err)
        }
    }
}

Because this sequential approach is slow, the article introduces concurrency using goroutines and a sync.WaitGroup . The scanning logic is extracted into an isOpen function that returns a boolean.

func isOpen(host string, port int) bool {
    time.Sleep(time.Millisecond * 1)
    conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", host, port))
    if err == nil {
        _ = conn.Close()
        return true
    }
    return false
}

The main function now launches a goroutine for each port, increments the WaitGroup counter, and collects open ports after all goroutines finish.

func main() {
    ports := []int{}
    wg := &sync.WaitGroup{}
    for port := 1; port < 100; port++ {
        wg.Add(1)
        go func() {
            opened := isOpen("google.com", port)
            if opened {
                ports = append(ports, port)
            }
            wg.Done()
        }()
    }
    wg.Wait()
    fmt.Printf("opened ports: %v\n", ports)
}

To avoid waiting indefinitely on unresponsive hosts, a timeout is added using net.DialTimeout . The isOpen function now accepts a timeout parameter, and the main routine passes a 200 ms timeout value.

func isOpen(host string, port int, timeout time.Duration) bool {
    conn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%d", host, port), timeout)
    if err == nil {
        _ = conn.Close()
        return true
    }
    return false
}

func main() {
    ports := []int{}
    wg := &sync.WaitGroup{}
    timeout := time.Millisecond * 200
    for port := 1; port < 100; port++ {
        wg.Add(1)
        go func(p int) {
            opened := isOpen("google.com", p, timeout)
            if opened {
                ports = append(ports, p)
            }
            wg.Done()
        }(port)
    }
    wg.Wait()
    fmt.Printf("opened ports: %v\n", ports)
}

Command‑line flexibility is added with the flag package, allowing users to specify the target hostname, start and end ports, and timeout without recompiling.

func main() {
    hostname := flag.String("hostname", "", "hostname to test")
    startPort := flag.Int("start-port", 80, "the port on which the scanning starts")
    endPort := flag.Int("end-port", 100, "the port at which the scanning ends")
    timeout := flag.Duration("timeout", time.Millisecond*200, "timeout")
    flag.Parse()

    ports := []int{}
    wg := &sync.WaitGroup{}
    for port := *startPort; port <= *endPort; port++ {
        wg.Add(1)
        go func(p int) {
            opened := isOpen(*hostname, p, *timeout)
            if opened {
                ports = append(ports, p)
            }
            wg.Done()
        }(port)
    }
    wg.Wait()
    fmt.Printf("opened ports: %v\n", ports)
}

The article notes a potential race condition when multiple goroutines append to the shared ports slice. A sync.Mutex is introduced to protect the slice during updates.

wg := &sync.WaitGroup{}
mutex := &sync.Mutex{}
for port := *startPort; port <= *endPort; port++ {
    wg.Add(1)
    go func(p int) {
        opened := isOpen(*hostname, p, *timeout)
        if opened {
            mutex.Lock()
            ports = append(ports, p)
            mutex.Unlock()
        }
        wg.Done()
    }(port)
}

In conclusion, the guide provides a compact, extensible TCP port scanner written in Go, demonstrating network programming fundamentals, concurrency patterns, timeout handling, command‑line parsing, and basic synchronization techniques.

ConcurrencyGoTCPNetwork ProgrammingtimeoutPort Scannerflag
360 Tech Engineering
Written by

360 Tech Engineering

Official tech channel of 360, building the most professional technology aggregation platform for the brand.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.