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

项目介绍

今天用 OpenCode 写一个 TCP 端口转发器(Port Forwarder)——把发到本地端口的流量透明地转发到指定的后端地址。这个工具在本地开发中非常实用:调试微服务、代理内网端口、穿透防火墙都可以用它。

  • 技术栈:Go 标准库(net、io、sync)
  • 代码量:约 70 行
  • 最终产物:一个二进制文件,tcpfwd -listen :8080 -target 192.168.1.100:3306 就能跑起来

准备工作

  • Go 1.21+
  • 已安装 OpenCode(终端执行 opencode 即可进入交互界面)

实践过程

第一步:描述需求,生成核心代码

打开终端,进入空项目目录,对 OpenCode 说:

> :用 Go 写一个 TCP 端口转发工具 tcpfwd,支持 -listen-target 两个命令行参数。监听本地端口,把每个 TCP 连接双向转发到目标地址。

OpenCode 立刻生成了 main.go,核心逻辑非常清晰——net.Listen 监听端口,net.Dial 连接目标,然后用 io.Copy 双向拷贝数据:

listener, _ := net.Listen("tcp", *listenAddr)
for {
    clientConn, _ := listener.Accept()
    go func() {
        targetConn, _ := net.Dial("tcp", *targetAddr)
        go io.Copy(targetConn, clientConn)
        io.Copy(clientConn, targetConn)
    }()
}

关键点:不需要自己写任何网络 I/O 循环,Go 的 io.Copy 把这件事做完了。OpenCode 比你更懂标准库。

第二步:补充边界处理

原生代码缺了错误处理和优雅关闭。继续指挥:

> :加上错误处理,用 log.Printf 打印连接失败日志。再加上信号处理,收到 Ctrl+C 时先关闭监听器,等待现有连接处理完再退出。

OpenCode 追加了 signal.Notifysync.WaitGroup,改造后的 main 函数:

sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)

go func() {
    <-sigCh
    fmt.Println("\n正在关闭...")
    listener.Close()
}()

var wg sync.WaitGroup
for {
    clientConn, err := listener.Accept()
    if err != nil {
        select {
        case <-sigCh:
            wg.Wait()
            return
        default:
            continue
        }
    }
    wg.Add(1)
    go func() {
        defer wg.Done()
        handleConnection(clientConn, *targetAddr)
    }()
}

关键点:用 select 区分"主动关闭导致的 Accept 错误"和"真正的网络错误"——这是自己写容易忽略的细节,OpenCode 自动补上了。

第三步:构建与验证

> :检查代码能否编译,帮我构建成二进制。

$ go build -o tcpfwd .

编译通过,零警告。起一个测试:

# 终端1:启动转发器
$ ./tcpfwd -listen :9999 -target httpbin.org:80

# 终端2:通过转发器发请求
$ curl http://localhost:9999/get

返回正常 JSON,转发器日志打印了连接信息。Ctrl+C 后程序等所有连接处理完毕才退出,优雅。

关键点:整个过程我一行代码都没手写,只是依次描述需求,OpenCode 负责设计、编码、排错。代码量很少,但 goroutine 生命周期管理这种容易写出 bug 的地方,AI 全部处理好了。

完整代码

package main

import (
	"flag"
	"fmt"
	"io"
	"log"
	"net"
	"os"
	"os/signal"
	"sync"
	"syscall"
)

func main() {
	listenAddr := flag.String("listen", ":8080", "监听地址")
	targetAddr := flag.String("target", "", "目标地址")
	flag.Parse()

	if *targetAddr == "" {
		fmt.Fprintf(os.Stderr, "用法: tcpfwd -listen :8080 -target 192.168.1.100:3306\n")
		os.Exit(1)
	}

	listener, err := net.Listen("tcp", *listenAddr)
	if err != nil {
		log.Fatalf("监听失败: %v", err)
	}
	defer listener.Close()

	fmt.Printf("端口转发已启动: %s -> %s\n", *listenAddr, *targetAddr)

	sigCh := make(chan os.Signal, 1)
	signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)

	go func() {
		<-sigCh
		fmt.Println("\n正在关闭...")
		listener.Close()
	}()

	var wg sync.WaitGroup

	for {
		clientConn, err := listener.Accept()
		if err != nil {
			select {
			case <-sigCh:
				wg.Wait()
				return
			default:
				log.Printf("接受连接失败: %v", err)
				continue
			}
		}

		wg.Add(1)
		go func() {
			defer wg.Done()
			handleConnection(clientConn, *targetAddr)
		}()
	}
}

func handleConnection(clientConn net.Conn, targetAddr string) {
	defer clientConn.Close()

	targetConn, err := net.Dial("tcp", targetAddr)
	if err != nil {
		log.Printf("连接目标失败: %v", err)
		return
	}
	defer targetConn.Close()

	var wg sync.WaitGroup
	wg.Add(2)

	go func() {
		defer wg.Done()
		io.Copy(targetConn, clientConn)
		targetConn.Close()
	}()

	go func() {
		defer wg.Done()
		io.Copy(clientConn, targetConn)
		clientConn.Close()
	}()

	wg.Wait()
}

小结

这个工具让我再次确认了用 OpenCode 开发 CLI 工具的高效模式:

先用一句话描述需求,让 AI 生成主干代码

再补充边界条件(错误处理、信号、退出逻辑),AI 自动填充细节

构建验证,中途发现问题直接用自然语言描述,AI 定位并修改

70 行代码,从零到跑通不到 4 分钟。如果是手写,光查 signal.Notifyio.Copy 的用法就得翻几分钟文档。OpenCode 的真正价值不是"替你写代码",而是"替你查文档 + 替你写样板代码 + 替你考虑边界情况"——你只需要专注于描述需求。