端口转发核心是用net.listen监听本地端口、net.dial拨号目标地址,并通过两个goroutine双向io.copy实现全双工数据搬运,需设read/writedeadline防阻塞、正确close防泄漏、参数化目标地址、非特权端口避权限问题。

端口转发核心逻辑:用 net.Listen + net.Dial 就够了
Go 实现端口转发不靠第三方库,两行核心操作就能跑通:监听本地端口,收到连接后立刻拨到目标地址。关键不是“怎么转发”,而是“怎么不丢包、不断连、不卡死”。
-
net.Listen("tcp", ":8080")启动监听,注意地址格式必须带:(比如":8080"或"127.0.0.1:8080"),写成"8080"会 panic 报错"listen tcp: address 8080: missing port in address" - 每个
Accept()到的net.Conn必须开 goroutine 处理,否则第二个连接会阻塞第一个的读写 - 目标地址不能写死在代码里——用命令行参数或环境变量传入,比如
target := flag.String("to", "127.0.0.1:9000", "forward target address")
双向数据流必须显式 copy,别信 io.Copy 一次搞定
很多人以为 io.Copy(dst, src) 转发完一边就完了,结果发现 HTTP 请求能发过去但响应收不到,或者 SSH 连上就断。根本原因是 TCP 是全双工,客户端→代理→服务端 和 服务端→代理→客户端 两条流要**同时、独立、持续**搬运。
- 必须启动两个 goroutine:一个用
io.Copy(lConn, rConn)搬数据从远端回本地,另一个用io.Copy(rConn, lConn)搬数据从本地到远端 - 两个
io.Copy都要加defer rConn.Close()和defer lConn.Close(),否则连接泄漏,跑一小时就"too many open files" - 别用
io.CopyN或自定义 buffer 大小来“优化”——默认 32KB buffer 已足够,改小反而增加 syscall 次数
超时和粘包无关,但没设 SetDeadline 会让代理变“僵尸”
没有超时控制的转发服务,在客户端异常断网、服务端崩溃、中间网络中断时,goroutine 会永远卡在 Read 或 Write 上,连接不释放,内存缓慢上涨。这不是粘包问题,是 socket 状态没管理。
- 对每个新接受的
lConn,立即调用lConn.SetDeadline(time.Now().Add(5 * time.Minute)),同样对rConn也设(建议略短,比如 4 分钟) - 不要只设
ReadDeadline—— 写失败(如对方已关闭)同样会阻塞,WriteDeadline也得设 - 错误检查必须区分
net.ErrClosed和超时:if err != nil && !errors.Is(err, net.ErrClosed)才记日志,否则满屏报错干扰排查
Linux 下绑定特权端口()需要额外权限
想把 80 或 443 转发出去?net.Listen("tcp", ":80") 在普通用户下直接失败,报错 "listen tcp :80: bind: permission denied"。这不是 Go 的锅,是内核限制。
立即学习“go语言免费学习笔记(深入)”;
- 最稳妥做法:用非特权端口(如
:8080)运行程序,再用iptables或systemd的Capabilities=CAP_NET_BIND_SERVICE提权 - 避免用
sudo go run main.go——权限过大,且开发时容易误操作影响系统 - Docker 场景下,加
--cap-add=NET_BIND_SERVICE并在容器内用setcap 'cap_net_bind_service=+ep' /app/proxy更安全
真正难的不是写通转发,是让每个连接在各种异常网络条件下都能干净退出。goroutine 是否泄漏、deadline 是否覆盖所有路径、close 是否成对触发——这些细节不打日志根本看不见。










