直接读 /proc/net/dev 是最稳定、无需 root、兼容性最好的方式,比 net.interfacestat() 更适合生产环境——尤其在容器或虚拟网卡场景下,后者常返回 0 或统计失真。

直接读 /proc/net/dev 是最稳定、无需 root、兼容性最好的方式,比 net.InterfaceStat() 更适合生产环境——尤其在容器或虚拟网卡场景下,后者常返回 0 或统计失真。
为什么 net.InterfaceStat() 在 Linux 上经常不准
它依赖内核对每个接口暴露的 struct rtnl_link_stats64,但很多虚拟设备(veth、docker0、br- 网桥)不完整填充该结构;容器内进程调用时,看到的是命名空间隔离后的视图,BytesRecv/BytesSent 往往是 0 或只含 loopback 流量。物理网卡如 eth0 在宿主机上能用,但一进容器就失效。
常见错误现象:
-
stat, _ := iface.Stat()返回的stat.BytesRecv == 0即使网卡明显有流量 - 在 Kubernetes Pod 里跑,
net.Interfaces()列出的接口名是eth0,但实际对应 veth,Stat()值长期不变 - Windows 下部分 Realtek 驱动不支持该接口,字段全为 0
应对方式:
- 先用
net.Interfaces()获取接口列表,再对每个接口做iface.Stat(),若BytesRecv == 0 && BytesSent == 0就跳过 - 加 fallback:检测到全零后,自动降级读
/proc/net/dev - 永远不要只依赖一个接口——比如同时采
eth0和docker0,再按业务逻辑判断哪条更可信
读 /proc/net/dev 的正确姿势
这个文件是内核导出的原始统计,字段固定、无抽象损耗,且所有 Linux 发行版都保证存在。关键是要解析对列、跳过 header、匹配目标接口行。
示例片段(带容错):
func readNetDev(ifaceName string) (rx, tx uint64, err error) {
data, err := os.ReadFile("/proc/net/dev")
if err != nil {
return 0, 0, err
}
scanner := bufio.NewScanner(strings.NewReader(string(data)))
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if !strings.HasPrefix(line, ifaceName+":") {
continue
}
parts := strings.Fields(line)
// rx_bytes 在第2列,tx_bytes 在第10列(空格分隔,索引从0开始)
if len(parts) >= 11 {
rx, _ = strconv.ParseUint(parts[1], 10, 64)
tx, _ = strconv.ParseUint(parts[9], 10, 64)
}
return rx, tx, nil
}
return 0, 0, fmt.Errorf("interface %s not found in /proc/net/dev", ifaceName)
}
注意点:
- 字段顺序严格:第1列是接口名(带冒号),第2列是
rx_bytes,第10列是tx_bytes(不是第9列!因为第1列后还有个空字段) - 别用
strings.Split(line, " ")—— 多空格会被拆成空字符串,导致索引错位;用strings.Fields()自动压缩空白 - 容器内读
/proc/net/dev看到的是宿主机视角,所以必须在宿主机上运行该 Go 程序,不能放进容器里去读自己的/proc
如何算出准确的实时带宽(Bps / Mbps)
带宽是差值除以时间,不是单次读数。两次采样间隔必须稳定,否则速率失真。
错误做法:time.Sleep(1 * time.Second) —— 每次解析+打印耗时几十毫秒,累积误差可达 ±15%。
正确做法:用 time.Ticker,并确保 ticker 循环体轻量:
- 只做
readNetDev()+ 差值计算 + 存入变量,**不要**在 ticker 循环里写日志、发 HTTP、序列化 JSON - 每次采样后立即记录
time.Now(),用time.Since(prevTime)算真实间隔,而非假设 1 秒 - 首次采样无差值,跳过输出;后续每次用
(curr - prev) / elapsed.Seconds() - 建议采样间隔 ≥ 2 秒:太短易受瞬时抖动影响,也增加 I/O 压力
例如 5 秒间隔下,真实间隔可能是 5.002134s,用这个值算才准。
容器场景下怎么拿到某容器的网络流量
不能靠容器内 /proc/net/dev,也不能靠 net.InterfaceStat()。得走“PID → cgroup → 进程级网络 I/O”路径:
- 先通过
docker inspect -f '{{.State.Pid}}' <cid></cid>或crictl inspect <cid></cid>拿到容器主进程 PID - 读
/proc/<pid>/net/dev</pid>—— 注意:这仍是宿主机视角,但只反映该进程打开的 socket 所用网卡的流量(通常就是eth0或ens3) - 如果容器用了多网卡(如 hostNetwork + bridge),需结合
/proc/<pid>/fd/</pid>下 socket 的readlinkinode,再查/proc/net/tcp匹配所属接口 - 更稳的替代方案:读该 PID 所属 cgroup v2 路径下的
io.stat(若启用 IO controller),但需确认运行时是否开启
最容易被忽略的一点:Go 程序必须运行在宿主机上,且有权限读取 /proc/<pid></pid> 目录(即不能是 rootless Docker 容器)。否则会遇到 permission denied 或空数据。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











