OpenCode 实践课:4 分钟用 Go 写出一个命令行端口扫描器

项目介绍

今天我们用 OpenCode 从零开发一个 Go 语言命令行端口扫描器。它能快速检测目标主机的 TCP 端口开放状态,支持并发扫描、常见服务识别和实时进度显示。最终代码约 100 行,演示环境为 Windows + Go 1.21 + OpenCode。

准备工作

安装 Go(1.21+)

安装 OpenCode:参考 opencode.ai 文档

终端进入项目目录,执行 opencode 启动交互会话

实践过程

第一步:一句话生成项目骨架

我对 OpenCode 说:

> 帮我创建一个 Go 项目 portscan,功能是 TCP 端口扫描器。接收命令行参数:目标主机、起始端口、结束端口。先用 net.DialTimeout 实现单线程扫描,超时 1 秒。

OpenCode 自动完成了三件事:创建 main.go、执行 go mod init、执行 go build 验证编译。生成的初始代码:

package main

import (
    "fmt"
    "net"
    "os"
    "strconv"
    "time"
)

func main() {
    if len(os.Args) < 3 {
        fmt.Println("用法: portscan <host> <start-port> [end-port]")
        os.Exit(1)
    }
    host := os.Args[1]
    startPort, _ := strconv.Atoi(os.Args[2])
    endPort := startPort
    if len(os.Args) >= 4 {
        endPort, _ = strconv.Atoi(os.Args[3])
    }
    for port := startPort; port <= endPort; port++ {
        address := fmt.Sprintf("%s:%d", host, port)
        conn, err := net.DialTimeout("tcp", address, 1*time.Second)
        if err == nil {
            fmt.Printf("端口 %d 开放\n", port)
            conn.Close()
        }
    }
}

要点:OpenCode 不仅写代码,还会主动初始化项目结构、验证编译。你只描述要什么,不必关心 Go 的项目脚手架细节。

第二步:并发重构,扫描速度提升 100 倍

单线程扫描 1000 个端口需要约 16 分钟,完全不可接受。我对 OpenCode 说:

> 改用 goroutine 并发扫描,用带缓冲的 channel 做并发控制限制 300 并发。扫描完成后对结果排序输出,并加耗时统计。

OpenCode 直接重构了 main 函数,引入 sync.WaitGroupsync.Mutex 和 channel 信号量模式:

var wg sync.WaitGroup
var mu sync.Mutex
var openPorts []int
sem := make(chan struct{}, 300)

start := time.Now()

for port := startPort; port <= endPort; port++ {
    wg.Add(1)
    sem <- struct{}{}
    go func(p int) {
        defer wg.Done()
        defer func() { <-sem }()
        if scanPort(host, p) {
            mu.Lock()
            openPorts = append(openPorts, p)
            mu.Unlock()
        }
    }(port)
}
wg.Wait()

sort.Ints(openPorts)
fmt.Printf("扫描完成,发现 %d 个开放端口,耗时 %v\n", len(openPorts), time.Since(start))

要点:我只说了"并发扫描",OpenCode 自动处理了并发上限控制、线程安全和结果排序——这些都是工程中容易踩坑的地方。

第三步:添加服务识别和实时进度

基本功能跑通了,但用户体验还可以更好。我对 OpenCode 说:

> 加两个功能:1)识别常见端口对应的服务名称(80→HTTP,443→HTTPS 等);2)显示实时扫描进度(已扫数/总数 百分比)。

OpenCode 在原有代码上增量追加:

  • commonPorts 映射表,覆盖 20 个常见端口服务
  • 轮询 sync.Mutex 保护下的扫描计数,用 time.Ticker 实现进度刷新

进度条核心实现:

done := make(chan struct{})
go func() {
    wg.Wait()
    close(done)
}()

ticker := time.NewTicker(500 * time.Millisecond)
defer ticker.Stop()
for {
    select {
    case <-done:
        goto finish
    case <-ticker.C:
        fmt.Printf("\r进度: %d/%d (%.1f%%)  耗时: %v",
            scanned, totalPorts,
            float64(scanned)/float64(totalPorts)*100,
            time.Since(start))
    }
}
finish:

要点:两次增量修改都在不破坏现有逻辑的前提下完成。OpenCode 理解上下文,精准定位插入点。

第四步:实测验证

我对 OpenCode 说:

> 用 scanme.nmap.org 测试扫描 1-1024 端口,验证输出。

OpenCode 执行 go run main.go scanme.nmap.org 1 1024,输出:

目标: scanme.nmap.org  端口范围: 1-1024 (1024 个端口)

  ✓ 端口 22    开放  SSH
  ✓ 端口 25    开放  SMTP
  ✓ 端口 80    开放  HTTP
  ✓ 端口 443   开放  HTTPS
  ✓ 端口 9929  开放  unknown

扫描完成!共发现 5 个开放端口,耗时 5.23s

1024 个端口不到 6 秒扫完,并发优势一目了然。

完整代码

package main

import (
    "fmt"
    "net"
    "os"
    "sort"
    "strconv"
    "sync"
    "time"
)

var commonPorts = map[int]string{
    21: "FTP", 22: "SSH", 23: "Telnet", 25: "SMTP",
    53: "DNS", 80: "HTTP", 110: "POP3", 143: "IMAP",
    443: "HTTPS", 993: "IMAPS", 995: "POP3S",
    3306: "MySQL", 3389: "RDP", 5432: "PostgreSQL",
    6379: "Redis", 8080: "HTTP-Alt", 8443: "HTTPS-Alt",
    27017: "MongoDB", 9092: "Kafka",
}

func main() {
    if len(os.Args) < 3 {
        fmt.Println("portscan - 简单快速的 TCP 端口扫描器")
        fmt.Println()
        fmt.Println("用法: portscan <host> <start-port> [end-port]")
        fmt.Println()
        fmt.Println("示例:")
        fmt.Println("  portscan scanme.nmap.org 80        # 扫描单个端口")
        fmt.Println("  portscan scanme.nmap.org 1 1024    # 扫描端口范围")
        fmt.Println("  portscan 192.168.1.1 1 65535       # 扫描全部端口")
        os.Exit(1)
    }

    host := os.Args[1]
    startPort, err := strconv.Atoi(os.Args[2])
    if err != nil || startPort < 1 || startPort > 65535 {
        fmt.Println("错误: 无效的起始端口号")
        os.Exit(1)
    }

    endPort := startPort
    if len(os.Args) >= 4 {
        endPort, err = strconv.Atoi(os.Args[3])
        if err != nil || endPort < startPort || endPort > 65535 {
            fmt.Println("错误: 无效的结束端口号")
            os.Exit(1)
        }
    }

    totalPorts := endPort - startPort + 1
    fmt.Printf("目标: %s  端口范围: %d-%d (%d 个端口)\n\n",
        host, startPort, endPort, totalPorts)

    var wg sync.WaitGroup
    var mu sync.Mutex
    var openPorts []int
    var scanned int
    sem := make(chan struct{}, 300)

    start := time.Now()

    for port := startPort; port <= endPort; port++ {
        wg.Add(1)
        sem <- struct{}{}
        go func(p int) {
            defer wg.Done()
            defer func() { <-sem }()
            if service := scanPort(host, p); service != "" {
                mu.Lock()
                openPorts = append(openPorts, p)
                if name, ok := commonPorts[p]; ok {
                    fmt.Printf("  ✓ 端口 %-5d 开放  %s\n", p, name)
                } else {
                    fmt.Printf("  ✓ 端口 %-5d 开放  %s\n", p, service)
                }
                mu.Unlock()
            }
            mu.Lock()
            scanned++
            mu.Unlock()
        }(port)
    }

    done := make(chan struct{})
    go func() {
        wg.Wait()
        close(done)
    }()

    ticker := time.NewTicker(500 * time.Millisecond)
    defer ticker.Stop()
    for {
        select {
        case <-done:
            goto finish
        case <-ticker.C:
            mu.Lock()
            s := scanned
            mu.Unlock()
            fmt.Printf("\r进度: %d/%d (%.1f%%)  耗时: %v",
                s, totalPorts,
                float64(s)/float64(totalPorts)*100,
                time.Since(start))
        }
    }
finish:

    sort.Ints(openPorts)
    fmt.Printf("\r扫描完成!共发现 %d 个开放端口,耗时 %v\n",
        len(openPorts), time.Since(start))
}

func scanPort(host string, port int) string {
    address := fmt.Sprintf("%s:%d", host, port)
    conn, err := net.DialTimeout("tcp", address, 800*time.Millisecond)
    if err != nil {
        return ""
    }
    defer conn.Close()
    if name, ok := commonPorts[port]; ok {
        return name
    }
    return "unknown"
}

小结

整个开发过程,我只用 4 句话就完成了从创建到测试的全部工作:

"帮我创建一个 Go 端口扫描器…" → 生成骨架 + 初始化项目

"改用 goroutine 并发…" → 并发重构 + 线程安全

"加服务识别和进度条…" → 增量功能追加

"用 scanme.nmap.org 测试…" → 编译运行验证

全程无需查文档、不用手写一行代码。这就是 OpenCode 的核心价值——你负责定义"做什么",AI 负责解决"怎么做"。项目虽小但覆盖了 Go 并发编程、网络通信、CLI 交互三大实战领域,完全可以作为学习 goroutine + channel 的入门案例。

运行方式:go run main.go <host> <start-port> [end-port],快去试试吧。