生产环境优先选 gobit(github.com/256dpi/gobit),因其支持并发读写、连接池、超时控制和事务id自动管理,而 mbgo 不支持并发且 tcp 复用有缺陷,易 panic 或返回乱码。

Go 里用 gobit 还是 modbus 包?选错会连不上设备
直接说结论:生产环境优先选 gobit(github.com/256dpi/gobit),不是更火的 modbus(github.com/tbrandon/mbgo)。后者不支持并发读写、TCP 连接复用逻辑有缺陷,遇到 PLC 频繁轮询或多个 goroutine 同时调用 ReadHoldingRegisters 会 panic 或返回乱码。
而 gobit 基于标准 Modbus TCP ADU 封装,底层用 net.Conn 管理连接,支持连接池和超时控制。它把协议细节藏在 Client 接口后,你只需关注功能码和地址——这才是 Go 应该有的抽象方式。
-
gobit的client.ReadHoldingRegisters(1, 0, 10)表示从从站 ID 1、地址 0 开始读 10 个寄存器,语义清晰 -
mbgo的client.ReadHoldingRegisters(0, 10)不显式传 slaveID,依赖 client 初始化时绑定,多设备场景容易串 - 如果设备只支持 RTU over RS485,
gobit需配合gorilla/serial手动构造帧;此时可退而求其次用mbgo的RTUClient,但务必加sync.Mutex保护串口读写
Modbus TCP 连接超时和重试必须自己加,库不会替你兜底
Modbus 协议本身没有心跳和重连机制,gobit 的 Client 默认使用无超时的 net.Dial,一旦网络抖动或设备断电,ReadCoils 会卡死数分钟,拖垮整个 goroutine。
正确做法是在 Dial 阶段注入自定义 net.Conn,用 net.DialTimeout 控制建立连接时间,并用 context.WithTimeout 包裹每次请求:
conn, err := net.DialTimeout("tcp", "192.168.1.10:502", 3*time.Second)
if err != nil {
return err
}
client := gobit.NewClient(conn)
// 每次读取也设超时
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
results, err := client.ReadHoldingRegisters(ctx, 1, 0, 10)
- 不要依赖
client.SetTimeout()(gobit没这方法)——那是老版本mbgo的坑,实际只控制 write,不控 read - 重试建议用指数退避:第一次失败等 100ms,第二次 200ms,最多 3 次;超过就报错,避免雪崩
- 注意:Modbus TCP 的事务标识符(Transaction ID)由客户端自增,
gobit内部自动维护,你无需手动同步
读到的寄存器值是字节序反的?别急着改 binary.BigEndian
常见现象:ReadHoldingRegisters 返回 []uint16{0x1234, 0x5678},但设备手册写的是浮点数 3.14,直接转成 float32 却得到完全不对的值。问题不在 Go,而在 Modbus 寄存器天然按 16 位整数组织,而浮点数/双精度需要跨寄存器拼接,且字节序规则由设备厂商定。
典型情况有三种:
- AB(Allen-Bradley)PLC:先高字再低字,即
0x1234 0x5678→0x12345678→math.Float32frombits(0x12345678) - Siemens S7:先低字再高字,即
0x5678 0x1234→0x56781234 - 有些国产仪表:高低字内部再翻转,比如
0x3412 0x7856,本质是寄存器内按小端存,再按大端传
解决办法不是猜,而是查设备 Modbus 地址表——它一定会注明“IEEE 754 float, big-endian register order” 或类似描述。没写清楚?拿已知值(比如当前温度 25.0℃)实测两组寄存器,暴力穷举四种组合。
并发读多个地址时,千万别用同一个 Client 实例
gobit.Client 内部没有锁,所有方法都非线程安全。如果你开 10 个 goroutine 同时调用 client.ReadInputRegisters(1, 100, 1),大概率触发 io.ErrUnexpectedEOF 或返回部分数据——因为多个请求的 TCP 包被混在一起发,响应帧也错乱了。
- 方案一(推荐):每个 goroutine 新建
Client,用完conn.Close()。短连接开销不大,TCP 握手在局域网通常 - 方案二:用
sync.Pool复用Client,但必须保证 Pool 中的 client 不被多个 goroutine 同时持有 - 绝对禁止:全局单例
var client *gobit.Client,这是最隐蔽的并发 bug 来源
Modbus 是主从协议,物理上就不支持“一个客户端同时问多个从站”,更别说一个连接并发问同一从站的不同地址——看似省资源,实则违反协议语义。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











