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

项目介绍

今天我们用 OpenCode 从零开发一个命令行端口扫描工具。输入目标 IP 和端口范围,它会并发探测哪些端口处于开放状态,并识别常见服务(如 HTTP、SSH、MySQL)。

  • 技术栈:Go 1.21+
  • 代码量:约 85 行
  • 最终效果
正在扫描 127.0.0.1 端口 1-1024...

开放端口:
─────────────────────────────
  22     SSH
  80     HTTP
  443    HTTPS
  3306   MySQL
─────────────────────────────

扫描完成:共扫描 1024 个端口,4 个端口开放,耗时 2.34 秒

准备工作

  • 安装 Go(go.dev
  • 安装 OpenCode(npm install -g @anthropic-ai/opencode 或参考 opencode.ai
  • 在终端执行 opencode 进入交互会话

实践过程

第一步:初始化项目骨架

我对 OpenCode 说:

> 帮我创建一个 Go 项目,用于扫描目标主机的开放端口。要求:支持通过命令行参数指定目标 IP、端口范围、连接超时和并发数,使用 goroutine 并发扫描。

OpenCode 立即创建了项目文件。我补充说明参数细节:

> 参数包括:--host 目标地址,--start 起始端口,--end 结束端口,--timeout 超时秒数,--workers 并发数。main 函数用 flag 包解析参数,创建工作协程池并发连接目标端口。

OpenCode 生成了如下核心代码:

ports := make(chan int, *workers)
results := make(chan int, *workers)
var wg sync.WaitGroup

for i := 0; i < *workers; i++ {
    wg.Add(1)
    go worker(*host, ports, results, &wg, time.Duration(*timeout)*time.Second)
}

要点:一次对话就把项目结构、参数解析、并发模型全部交代清楚。OpenCode 自动处理了 channel 的创建与关闭,以及 WaitGroup 同步。

第二步:添加服务名称识别

扫描出端口号不够直观,我让 OpenCode 加上服务名映射:

> 加上常用端口对应的服务名称识别,比如 22→SSH、80→HTTP、443→HTTPS、3306→MySQL、6379→Redis、5432→PostgreSQL、8080→HTTP-Alt。定义一个 map 映射表,输出结果时显示对应服务名。

OpenCode 理解了需求,补充了 commonPorts 映射表,并将输出格式改为:

service, ok := commonPorts[r.port]
if !ok {
    service = "Unknown"
}
fmt.Printf("  %-6d %s\n", r.port, service)

要点:用自然语言描述数据映射关系,OpenCode 能准确翻译成 Go 的 map[int]string 结构。

第三步:优化输出与统计

基础功能跑通后,我希望输出更专业:

> 优化输出效果:扫描开始时打印提示信息;扫描结果按端口号排序输出,用分隔线美化;最后汇总显示总扫描数、开放数、耗时。

OpenCode 添加了 sort.Slice 对结果排序,用 time.Since 统计耗时,并格式化输出了分隔线和汇总信息。完整的主流程如下:

var openPorts []scanResult
startTime := time.Now()

for result := range results {
    openPorts = append(openPorts, result)
}

elapsed := time.Since(startTime)
sort.Slice(openPorts, func(i, j int) bool {
    return openPorts[i].port < openPorts[j].port
})

fmt.Printf("\n扫描完成:共扫描 %d 个端口,%d 个端口开放,耗时 %.2f 秒\n",
    total, len(openPorts), elapsed.Seconds())

要点:优化类需求只需描述期望的最终效果,OpenCode 会自动选择合适的数据结构和算法。

完整代码

package main

import (
	"flag"
	"fmt"
	"net"
	"sort"
	"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",
}

type scanResult struct {
	port int
}

func main() {
	host := flag.String("host", "127.0.0.1", "目标主机地址")
	startPort := flag.Int("start", 1, "起始端口")
	endPort := flag.Int("end", 1024, "结束端口")
	timeout := flag.Int("timeout", 1, "连接超时(秒)")
	workers := flag.Int("workers", 100, "并发工作协程数")
	flag.Parse()

	ports := make(chan int, *workers)
	results := make(chan scanResult, *workers)
	var wg sync.WaitGroup

	for i := 0; i < *workers; i++ {
		wg.Add(1)
		go worker(*host, ports, results, &wg, time.Duration(*timeout)*time.Second)
	}

	go func() {
		for port := *startPort; port <= *endPort; port++ {
			ports <- port
		}
		close(ports)
	}()

	go func() {
		wg.Wait()
		close(results)
	}()

	var openPorts []scanResult
	startTime := time.Now()

	fmt.Printf("\n正在扫描 %s 端口 %d-%d...\n\n", *host, *startPort, *endPort)

	for result := range results {
		openPorts = append(openPorts, result)
	}

	elapsed := time.Since(startTime)

	sort.Slice(openPorts, func(i, j int) bool {
		return openPorts[i].port < openPorts[j].port
	})

	fmt.Println("开放端口:")
	fmt.Println("─────────────────────────────")
	for _, r := range openPorts {
		service, ok := commonPorts[r.port]
		if !ok {
			service = "Unknown"
		}
		fmt.Printf("  %-6d %s\n", r.port, service)
	}
	fmt.Println("─────────────────────────────")

	total := *endPort - *startPort + 1
	fmt.Printf("\n扫描完成:共扫描 %d 个端口,%d 个端口开放,耗时 %.2f 秒\n",
		total, len(openPorts), elapsed.Seconds())
}

func worker(host string, ports <-chan int, results chan<- scanResult, wg *sync.WaitGroup, timeout time.Duration) {
	for port := range ports {
		address := fmt.Sprintf("%s:%d", host, port)
		conn, err := net.DialTimeout("tcp", address, timeout)
		if err == nil {
			conn.Close()
			results <- scanResult{port: port}
		}
	}
	wg.Done()
}

使用方式:

go run main.go --host 127.0.0.1 --start 1 --end 1024
go run main.go --host 192.168.1.1 --start 80 --end 443 --timeout 2

小结

这个端口扫描器从想法到可运行只花了 5 分钟,三句自然语言对话完成。相比手写,省去了查 flag 用法、channel 语法、sync 包 API 的时间——这些细节 OpenCode 全部自动处理。

回顾这次实践,OpenCode 的价值不在于"替你写代码",而在于消除编码过程中的摩擦:你专注描述"要做什么",它负责翻译成"怎么做"。对于小工具开发、原型验证这类场景,效率提升显著。

编译运行试试看,扫一下自己本机的开放端口,你会对结果感到惊讶的。