固定目标用net.dialudp,动态或多目标必须用net.listenudp+writetoudp;前者省事但需预解析地址并注意icmp错误延迟,后者符合udp本质、需显式指定每次目标且须设写超时防goroutine卡死。

net.DialUDP 和 net.ListenUDP + WriteToUDP 是 Go 中发送 UDP 数据的两条路径,选哪条取决于目标地址是否固定——固定就用 DialUDP,动态或多个目标必须用 ListenUDP 配合 WriteToUDP。
用 DialUDP 发固定目标:省事但别乱传地址
适合发给单个已知地址(比如本地测试服务 "127.0.0.1:8080"),它返回一个“已连接”的 *UDPConn,后续调 Write 就自动发过去。
-
net.ResolveUDPAddr("udp", "127.0.0.1:8080")必须先做,不能直接把字符串塞给DialUDP;写成"localhost:8080"可能解析出 IPv6 地址::1,而接收端只监听 IPv4 就收不到 - 第二个参数(本地绑定地址)传
nil让系统自动选端口,别硬写&net.UDPAddr{Port: 0}—— 看似等价,但某些旧内核下会触发bind: invalid argument -
conn.Write([]byte("hi"))成功只代表数据交给了内核,不等于对方收到;若目标端口没监听,Linux 下第二次Write可能返回connection refused(ICMP 错误延迟上报)
用 ListenUDP + WriteToUDP 发动态目标:这才是 UDP 的本质
只要目标 IP 或端口可能变化(比如设备发现、NAT 打洞、广播),就必须走这条路。它创建的是“未连接”连接,每次发包都得显式指定目标地址。
-
net.ListenUDP("udp", &net.UDPAddr{Port: 0})是标准客户端起法,让系统随机分配本地端口;别用":0",Go 不支持这种写法 -
WriteToUDP第二个参数必须是有效的*net.UDPAddr,传nil或格式错(如 IPv4 地址配 IPv6 端口)会 panic:write: invalid argument - 别在 for 循环里反复调
net.ResolveUDPAddr解析 DNS 域名——开销大,应提前解析并缓存;若目标 IP 频繁变动,需加 TTL 控制重解析节奏
WriteToUDP 不设超时会卡死 goroutine
UDPConn 默认阻塞,WriteToUDP 在网络异常(如路由黑洞、防火墙静默丢包)时可能挂住数分钟,导致 goroutine 积压。
- 务必在
WriteToUDP前调SetWriteDeadline(time.Now().Add(2 * time.Second));用SetWriteTimeout也行,但 Deadline 更易与上下文配合 - 错误检查不能只看
err != nil:要区分net.ErrWriteTimeout(可重试)和真实网络错误(如no route to host) - 别在 defer 里设 deadline——它只对下一次 I/O 生效,不是长期策略
缓冲区大小和空包处理常被忽略
UDP 包可能被截断,也可能收到零长度数据报,这两类问题不会报错,但会直接破坏业务逻辑。
- 发送端缓冲区建议 1500 字节(匹配以太网 MTU),避免 IP 分片;接收端缓冲区至少设为 65536,否则大包被内核无声截断
-
ReadFromUDP返回n == 0 && err == nil就是空包,常见于客户端调sendto(..., 0);不判断就直接json.Unmarshal(buf[:n], &v)会解出零值甚至 panic - 别依赖
conn.RemoteAddr()——它永远是nil,UDP 没连接状态;每次响应必须用ReadFromUDP返回的addr,硬编码或复用监听地址必发错
真正难的不是写通,而是当服务从本机跑向局域网、再上到云服务器时,那些藏在系统 UDP 缓冲区、防火墙规则、NAT 超时和 DNS 解析缓存里的行为差异。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











