位掩码权限不是布尔数组替代品,其核心价值是将权限判断压缩为一次 & 运算,适用于固定、离散、组合频繁的权限场景,不适用于动态权限(如按ID授权),后者需RBAC或ABAC补充。

位掩码权限不是布尔数组替代品
直接用 []bool 或 map[string]bool 存权限,看着直观,但一到高并发鉴权或批量用户查询就卡住。位掩码的核心价值是把「是否有某权限」压缩成一次 & 运算,而不是省几行代码。它天然适合固定、离散、组合频繁的权限场景,比如「用户模块可读+可编辑+不可删」「订单模块仅查看」这种明确边界的操作集合。
常见错误是拿位掩码硬套动态权限(如「允许访问ID为123的订单」),结果发现无法用 & 判断——因为 123 不是 2 的幂。这类需求必须交给 RBAC 或 ABAC 补充,位掩码只管「能力类型」,不管「能力作用对象」。
Go 中定义和使用权限常量的正确姿势
别手写 1、2、4,用左移确保语义清晰且不易错:
const (
PermRead = 1 << iota // 0b0001
PermWrite // 0b0010
PermDelete // 0b0100
PermAdmin // 0b1000
)
这样定义后,组合权限用 |,判断用 &:
立即学习“go语言免费学习笔记(深入)”;
-
userMask & PermRead != 0→ 是否有读权限 -
userMask & (PermRead | PermWrite) == (PermRead | PermWrite)→ 是否同时具备读写 -
userMask |= PermDelete→ 授予删除权限 -
userMask &^= PermAdmin→ 撤销管理员权限(&^是 Go 特有 AND-NOT,比& (^PermAdmin)更安全)
注意:一旦上线,PermRead 永远不能改值,否则历史数据全失效;新增权限只能追加在 iota 后面,不能插在中间。
数据库字段类型与越界风险
MySQL 中 TINYINT 最大支持 8 位(0–255),意味着最多存 8 种独立权限。超过就得换:
- 16 个权限以内:用
SMALLINT UNSIGNED - 32 个以内:用
INT UNSIGNED - 真要超 32 个,别硬撑——说明权限设计已偏离位掩码适用场景,该拆模块或引入 RBAC 分层了
Go struct 映射时,字段类型必须匹配数据库精度,比如 DB 用 INT,Go 就得用 uint32,别用 int ——负数右移会高位补 1,导致位运算结果全毁。
对外暴露权限时别直接传 mask 整数
API 返回 {"permission_mask": 7} 对前端毫无意义。应在服务层转成可读列表:
func maskToPermissions(mask uint32) []string {
var perms []string
if mask&PermRead != 0 { perms = append(perms, "read") }
if mask&PermWrite != 0 { perms = append(perms, "write") }
if mask&PermDelete != 0 { perms = append(perms, "delete") }
return perms
}
更进一步,可预定义常用组合常量:PermUserEditor = PermRead | PermWrite,业务代码里直接复用,避免到处拼 |。
真正容易被忽略的不是怎么设权限,而是权限含义变更后如何安全迁移:上线前必须确认所有角色的 permission_mask 值在新旧定义下逻辑等价,否则一个 ALTER COLUMN 就会让整套鉴权失效。


















