必须用os.OpenFile显式控制标志位打开设备文件,禁用O_CREATE和O_TRUNC,选用O_WRONLY或O_RDWR,权限参数无效,需验证设备路径、用户权限及是否被占用。

直接用 os.OpenFile 打开设备文件,别用 os.WriteFile
os.WriteFile 内部会先 open、再 write、最后 close,对普通文件没问题,但对 /dev/sda、/dev/ttyS0 这类设备文件,它默认使用 os.O_TRUNC(截断)和 os.O_CREATE(创建),而设备文件不能“创建”,也不该被截断——这会导致 open: operation not supported 或 permission denied 错误。
必须显式用 os.OpenFile 控制标志位:
-
os.O_WRONLY或os.O_RDWR:根据设备需求选只写或读写 - 去掉
os.O_CREATE和os.O_TRUNC:设备文件已存在,无需创建,也不能清空 - 权限参数(
perm)实际无效:设备文件的权限由内核和 udev 规则决定,传0600也改不了
写入前确认设备路径、权限与访问模式
Linux 下向设备写原始数据失败,90% 是卡在这三步没验证:
- 路径是否真实存在:
ls -l /dev/sdb看有没有对应节点,stat /dev/sdb确认是block设备而非符号链接 - 当前用户是否有权访问:
sudo ls -l /dev/sdb查看属组(如disk),然后sudo usermod -aG disk $USER加组并重登 - 是否被其他进程占用:
lsof /dev/sdb或fuser -v /dev/sdb,若显示dd、parted正在用,得先停掉
特别注意:串口类设备(如 /dev/ttyUSB0)需额外配置波特率、停止位等,os.File.Write 只发原始字节,不处理串口协议——得用 github.com/tarm/serial 这类封装库,否则可能写进去但设备不响应。
立即学习“go语言免费学习笔记(深入)”;
写入时必须检查 Write 和 Close 的返回值
向设备写数据不是“发出去就完事”。常见现象包括:
-
Write返回字节数小于预期:说明内核缓冲区满或设备忙,比如向满载的 NAND flash 写入时可能只接受部分数据 -
Write返回io.ErrShortWrite:需手动重试未写完的部分,标准库不会自动补全 -
Close报错:设备驱动在刷盘或校验阶段失败(例如向只读 SD 卡写入后 Close 时报read-only file system)
示例关键片段:
file, err := os.OpenFile("/dev/sdb", os.O_WRONLY, 0)
if err != nil {
log.Fatal(err) // 不要用 fmt.Print,错误信息不完整
}
defer file.Close()
n, err := file.Write(data)
if err != nil {
log.Fatal("Write failed:", err)
}
if n < len(data) {
log.Fatal("short write:", n, "/", len(data))
}
err = file.Close() // Close 可能因 flush 失败而报错,不能省
if err != nil {
log.Fatal("Close failed:", err)
}
大块原始数据写入要考虑缓冲与对齐
向块设备(如磁盘)写原始数据时,内核通常要求:
- 偏移量对齐到逻辑扇区边界(通常是 512 或 4096 字节)
- 写入长度是扇区大小的整数倍
- 否则
Write可能返回invalid argument
解决办法不是硬凑对齐,而是用 os.Seek 定位 + 分块写:
- 先
file.Seek(offset, io.SeekStart)跳到目标扇区起始位置 - 每次
Write传入恰好一个扇区的数据(如make([]byte, 4096)) - 避免跨扇区写,尤其不要用
bufio.Writer—— 它的缓冲机制会打乱原始字节流顺序,设备无法识别
真正难的不是写进去,而是确保写进去的每一个字节都按你预期的位置、格式、时序到达设备寄存器。调试时优先用 strace -e trace=open,write,close ./your-program 看系统调用是否符合预期。


















