如何使用Go获取Windows系统上物理USB端口的数量(不受设备插拔影响)
如何使用Go获取Windows系统上物理USB端口的数量(不受设备插拔影响)
我太懂你要的是什么了——就是不管插没插U盘、鼠标这些设备,这个数字都纹丝不动的真·物理USB端口数。之前用WMI查Win32_USBHub那套确实不靠谱,那些返回的都是系统枚举出来的逻辑节点,设备一拔就没了,完全不符合需求。咱们换个底层思路,直接从硬件本身的属性入手就行。
为什么之前的方法不管用?
WMI的那些类返回的是已被系统识别的逻辑设备/节点,空端口很多时候不会被主动枚举,所以设备一拔,对应的逻辑节点就消失了,数量自然跟着变。而物理端口是硬件自带的属性,写在集线器(包括主板上的根集线器)的硬件描述符里,不管有没有设备连接,这个数字都是固定死的。
正确的Windows API路径
要拿到物理端口数,得绕开WMI,直接调用USB底层设备API,核心步骤是:
- 用
SetupAPI枚举系统中所有的USB集线器(包括主板自带的根集线器和外接的USB集线器) - 对每个集线器,通过
DeviceIoControl发送IOCTL_USB_GET_HUB_DESCRIPTOR控制码,读取它的硬件描述符 - 描述符里的
bNbrPorts字段,就是这个集线器的物理端口总数,把所有集线器的这个数值加起来,就是系统的总物理USB端口数
Go实现的代码示例和说明
Go可以通过golang.org/x/sys/windows包轻松调用Windows API,下面是完整的可运行代码,我给你标了关键细节:
package main import ( "fmt" "syscall" "unsafe" "golang.org/x/sys/windows" ) const ( // 用来获取集线器描述符的控制码 IOCTL_USB_GET_HUB_DESCRIPTOR = 0x220400 // USB集线器设备接口的GUID字符串 GUID_DEVINTERFACE_USB_HUB_STRING = "{F18A0E88-C30C-11D0-8815-00A0C906BED8}" ) var ( GUID_DEVINTERFACE_USB_HUB = windows.GUID{} ) // USB集线器描述符结构体,我们只需要前几个字段 type USB_HUB_DESCRIPTOR struct { bLength byte // 描述符长度 bDescriptorType byte // 描述符类型 bNbrPorts byte // 物理端口数(核心字段) wHubCharacteristics uint16 // 集线器特性 bPwrOn2PwrGood byte // 电源开启到稳定的时间 bHubContrCurrent byte // 集线器所需电流 } func init() { // 初始化GUID err := windows.GUIDFromString(GUID_DEVINTERFACE_USB_HUB_STRING, &GUID_DEVINTERFACE_USB_HUB) if err != nil { panic(err) } } func getUSBPortsCount() (int, error) { totalPorts := 0 // 1. 枚举系统中所有存在的USB集线器设备 hDevInfo := windows.SetupDiGetClassDevs(&GUID_DEVINTERFACE_USB_HUB, nil, 0, windows.DIGCF_DEVICEINTERFACE|windows.DIGCF_PRESENT) if hDevInfo == windows.InvalidHandle { return 0, syscall.GetLastError() } defer windows.SetupDiDestroyDeviceInfoList(hDevInfo) // 遍历每个集线器设备 var deviceIndex uint32 for { var ifaceData windows.SP_DEVICE_INTERFACE_DATA ifaceData.CbSize = uint32(unsafe.Sizeof(ifaceData)) // 枚举下一个设备接口 success := windows.SetupDiEnumDeviceInterfaces(hDevInfo, nil, &GUID_DEVINTERFACE_USB_HUB, deviceIndex, &ifaceData) if !success { err := syscall.GetLastError() if err == syscall.ERROR_NO_MORE_ITEMS { break // 枚举完所有设备了 } return 0, err } // 获取设备路径的内存大小 var requiredSize uint32 windows.SetupDiGetDeviceInterfaceDetail(hDevInfo, &ifaceData, nil, 0, &requiredSize, nil) if requiredSize == 0 { deviceIndex++ continue } // 分配内存存储设备路径 detailData := (*windows.SP_DEVICE_INTERFACE_DETAIL_DATA_A)(unsafe.Pointer(windows.Calloc(1, uintptr(requiredSize)))) if detailData == nil { return 0, syscall.ENOMEM } defer windows.Free(unsafe.Pointer(detailData)) detailData.CbSize = uint32(unsafe.Sizeof(*detailData)) var devInfoData windows.SP_DEVINFO_DATA devInfoData.CbSize = uint32(unsafe.Sizeof(devInfoData)) // 拿到设备的实际路径 success = windows.SetupDiGetDeviceInterfaceDetail(hDevInfo, &ifaceData, detailData, requiredSize, nil, &devInfoData) if !success { deviceIndex++ continue } // 打开设备句柄,需要管理员权限 devicePath := syscall.StringToUTF8(detailData.DevicePath) hDevice, err := windows.CreateFile( syscall.StringToUTF16Ptr(string(devicePath)), windows.GENERIC_READ|windows.GENERIC_WRITE, windows.FILE_SHARE_READ|windows.FILE_SHARE_WRITE, nil, windows.OPEN_EXISTING, windows.FILE_ATTRIBUTE_NORMAL|windows.FILE_FLAG_OVERLAPPED, 0, ) if err != nil { deviceIndex++ continue } defer windows.CloseHandle(hDevice) // 发送IOCTL获取集线器描述符 var hubDesc USB_HUB_DESCRIPTOR var bytesReturned uint32 success = windows.DeviceIoControl( hDevice, IOCTL_USB_GET_HUB_DESCRIPTOR, nil, 0, unsafe.Pointer(&hubDesc), uint32(unsafe.Sizeof(hubDesc)), &bytesReturned, nil, ) if success && bytesReturned >= 3 { // 确保读到了bNbrPorts字段 totalPorts += int(hubDesc.bNbrPorts) } deviceIndex++ } return totalPorts, nil } func main() { ports, err := getUSBPortsCount() if err != nil { fmt.Printf("获取物理USB端口数失败: %v\n", err) return } fmt.Printf("系统总物理USB端口数: %d\n", ports) }
代码关键点说明
- GUID_DEVINTERFACE_USB_HUB:这个GUID专门用来定位系统中所有的USB集线器,包括主板上的根集线器(对应主板自带的USB口)和外接的USB集线器
- IOCTL_USB_GET_HUB_DESCRIPTOR:通过这个控制码直接读取集线器的硬件描述符,
bNbrPorts字段是硬件出厂时就写死的,完全不受设备插拔影响 - 管理员权限:必须用管理员身份运行程序,因为要访问底层设备的IO控制接口,普通权限会被系统拒绝
额外注意事项
- 如果你只想统计主板自带的USB端口(排除外接集线器),可以在枚举时过滤:根集线器的硬件ID通常包含
ROOT_HUB或ROOT_HUB20关键字,你可以通过SetupDiGetDeviceRegistryProperty查询设备的硬件ID来过滤 - 当你拔掉外接USB集线器时,程序返回的总数会减少——这是合理的,因为外接集线器的物理端口已经从系统中移除了;而主板上的根集线器端口数,不管你插不插设备,都会稳定统计
- 这个方法对USB 2.0/3.0/4.0都适用,不同版本的控制器对应的根集线器都会被正确枚举
关于Windows USB拓扑的小补充
Windows的USB系统是树状结构:
- 最顶层是USB主机控制器(主板上的硬件芯片)
- 每个主机控制器自带一个根集线器,根集线器的端口就是你在主板上看到的物理USB口
- 根集线器的每个端口可以连接设备,也可以连接外接USB集线器
- 外接集线器又有自己的物理端口,继续组成树的分支
所有这些集线器的物理端口数,都存在各自的硬件描述符里,这就是我们能拿到固定数值的核心原因。




