Go 在 Windows 发出 UDP 包却返回 n=0,别立刻重发
Go 1.27.2 的 Windows WriteMsgUDP 同步完成路径可能漏报发送字节数。核对同一数据报,按控制信息与 socket 连接方式选择处理。
Windows 上调用 UDPConn.WriteMsgUDP,发送非空数据后返回 n=0, err=nil,接收端却能读到完整数据报。若代码把零字节数当成“尚未发送”,可能不停重发同一个包。
Go 1.27.2 的 Windows 路径存在这个字节数回报问题。它与 UDP 本来就不保证送达不同:这里是本地发送已经完成,却没有正确取得同步完成的长度,不能据 n=0 推断这个数据报没有离开进程。
先对照一个数据报的发送与接收
故障特征可以表现为:
WriteMsgUDP: n=0 oobn=0 err=<nil>; payload length=5
ReadFromUDP: n=5 payload="hello"
这描述的是受影响路径的现象,不是每次调用的必然结果。Windows overlapped I/O 可能立即完成,也可能稍后通过完成端口报告;两条路径取得字节数的方式不同,所以不能因某一次返回正常就排除问题。
1.27.2 的 internal/poll 在三种 WriteMsg 发送路径调用 WSASendMsg 时,没有把用于接收立即完成字节数的参数传进去。稍后完成的操作可以从 overlapped result 取得长度,立即完成的分支却可能直接返回初始化的零。该问题也涉及 WriteMsgUDPAddrPort,换一个地址类型不等于绕过底层 WriteMsg。
诊断时保留工具链版本、Windows 版本、UDP 地址族、是否使用控制信息,以及每个数据报的业务序号。仅比较总字节数可能混入丢包或重复包;必须对照同一序号。不要在生产端无限发送,试图靠次数碰到这个调度分支。
不需要控制信息时,可以选择 WriteToUDP
若 socket 来自 ListenUDP,尚未通过 Dial 连接到固定对端,而且发送不需要 ancillary data,可以评估用 WriteToUDP 代替 WriteMsgUDP(payload, nil, peer)。1.27.2 的这条 Windows 发送路径使用 WSASendto,会传入字节数的接收地址。
下面是独立函数片段,conn 应是应用已有的未连接 UDPConn,peer 是已经校验过的目标地址:
import (
"io"
"net"
)
func sendDatagram(conn *net.UDPConn, peer *net.UDPAddr, payload []byte) error {
n, err := conn.WriteToUDP(payload, peer)
if err != nil {
return err
}
if n != len(payload) {
return io.ErrShortWrite
}
return nil
}
示例只检查这次本地写入的返回值;连接的 deadline、所有权和关闭仍由原调用方管理。它不证明远端已收到数据,也不应自动重试返回错误的包。已经连接的 UDP socket 应采用符合其连接语义的发送 API,不能把 WriteToUDP 直接套上去。
需要控制信息的程序不能丢掉 oob 参数来消除症状,那可能改变源地址、接口或其他协议行为。此时应采用经正式版本核实的修复,或按自己的投递协议识别序号、超时与重试;不能在调用成功后擅自把零改写为 payload 长度,也不能全局忽略任何短写。
截至 2026 年 10 月 23 日,已核对的 Go 1.27.2 正式源码仍有上述字节数回报缺口。选择其他工具链时,应核对正式发行和相应实现,并确认实际 go version,保留旧构建与依赖信息。
验收应在目标 Windows 环境同时检查发送返回数、接收内容和业务序号,覆盖原来的 WriteMsg 用法及选定的替代路径。若还出现 WSAEFAULT、消息过大或非 nil error,要按新的错误处理;不要把所有 UDP 发送异常都归给这个长度回报问题。


