Go中更安全的枚举

server/2024/11/13 15:25:05/

iota

Go让你用iota来使用枚举。

const (Guest = iotaMemberModeratorAdmin
)

虽然Go是明确的,但iota似乎相对模糊。如果你以任何其他方式对const组进行排序,你会引入副作用。在上面的例子中,你仅仅对第一个参数Guest赋值了。你可以显式地给每个值分配一个数字来避免这个问题,但这使iota变得过时。
iota对于用位运算定义的参数也很有效。

const (Guest = 1 << iota // 1Member            // 2Moderator         // 4Admin             // 8
)// ...user.Roles = Member | Moderator // 6

位掩码是有效的,有时也很有帮助。然而,在大多数Web应用程序中,它的使用情况与枚举不同。通常情况下,你可以将所有的角色存储在一个列表中。它也会更容易阅读。

iota的主要问题是它在整数上工作,没有防止传递无效的值。

func CreateUser(role int) error {fmt.Println("Creating user with role", role)return nil
}func main() {err := CreateUser(-1)if err != nil {fmt.Println(err)}err = CreateUser(42)if err != nil {fmt.Println(err)}
}

CreateUser会很乐意接受-1或42,即使没有相应的角色。

当然,我们可以在函数中验证这一点。但我们使用的是一种具有强类型的语言,所以让我们利用它。在我们应用程序的上下文中,用户角色远不止是一个模糊的数字。

反模式:整数枚举

不要使用基于iota的整数来表示不是连续的数字或标志的枚举。

我们可以引入一个类型来改进解决方案。

type Role uintconst (Guest Role = iotaMemberModeratorAdmin
)

它看起来更好,但仍有可能传递任何任意的整数来代替Role。Go编译器在这里并没有帮助我们。

func CreateUser(role Role) error {fmt.Println("Creating user with role", role)return nil
}func main () {err := CreateUser(0)if err != nil {fmt.Println(err)}err = CreateUser(role.Role(42))if err != nil {fmt.Println(err)}
}

这个类型是对裸整数的改进,但它仍然是一种幻觉。它并没有给我们提供任何保证,说明这个角色是有效的。

哨兵值

因为iota从0开始,Guest也是角色的零值。这使得我们很难检测到角色是空的还是有人传递了一个Guest值。
你可以通过从1开始计算来避免这种情况。甚至更好的是,保留一个明确的哨兵值,你可以进行比较,不能误认为是一个实际的角色。

const (Unknown Role = iotaGuestMemberModeratorAdmin
)
func CreateUser(r role.Role) error {if r == role.Unknown {return errors.New("no role provided")}fmt.Println("Creating user with role", r)return nil
}

策略:明确的哨兵 为枚举的零值保留一个显式变量。

更准确的描述

枚举似乎是关于连续的整数,但它很少是有效的表示。在网络应用中,我们使用枚举来分组某种类型的可能变体。它们并不能很好地映射到数字上。
当你在API响应、数据库表或日志中看到一个3时,很难理解其背景。你必须检查源码或过时的文档才能知道它是怎么回事。
在大多数情况下,字符串比整数更有意义。无论你在哪里看到它,一个有明确意义的表达都是显而易见的。既然iota无论如何也帮不了我们,我们还可以使用人类可读的字符串。

type Role stringconst (Unknown   Role = "unknown"Guest     Role = "guest"Member    Role = "member"Moderator Role = "moderator"Admin     Role = "admin"
)

策略:使用字符串值而不是整数。 避免使用空白,以方便解析和记录。使用camelCase、snake_case或kebab-case。

这样的表达对错误代码特别有用。{“error”:“user-not-found”}与{“error”:4102}相比是显而易见的.
然而,该类型仍然可以容纳任何任意的字符串。

err = CreateUser("super-admin")
if err != nil {fmt.Println(err)
}

基于结构的枚举

最后的迭代使用了结构体。它可以让我们在设计上保证代码的安全性。我们不需要检查传递的值是否正确。

type Role struct {slug string
}func (r Role) String() string {return r.slug
}var (Unknown   = Role{"unknown"}Guest     = Role{"guest"}Member    = Role{"member"}Moderator = Role{"moderator"}Admin     = Role{"admin"}
)

因为slug字段是私有的,所以不可能从包的外部引用它。你能构建的唯一无效的角色是空的:Role{}。

我们可以添加一个构造函数来创建一个基于slug的有效角色:

func FromString(s string) (Role, error) {switch s {case Guest.slug:return Guest, nilcase Member.slug:return Member, nilcase Moderator.slug:return Moderator, nilcase Admin.slug:return Admin, nil}return Unknown, errors.New("unknown role: " + s)
}

策略:基于结构的枚举 将枚举封装在结构中以获得额外的编译时安全性。

这种方法在你处理业务逻辑时是完美的。保持结构在内存中始终处于有效状态,使你的代码更容易操作和理解。检查枚举类型是否为空就足够了,而且你可以确定它是一个正确的值。
这种方法有一个潜在的问题。如果我们用的是全局性的常亮,这样的做法需要不断的赋值,如下:

roles.Guest = role.Admin

这样的话,这个值说不准在哪里变化了都不知道。

校验方法

竟然上面的方法都无法满足我们的需求,那么我们就加上一个校验方法,避免运行时传入了非法的值即可:

type Role stringconst (Unknown   Role = "unknown"Guest     Role = "guest"Member    Role = "member"Moderator Role = "moderator"Admin     Role = "admin"
)var roleSet = []Role{Unknown, Guest, Member, Moderator, Admin}func (role Role) Valid() bool {for _, r := range roleSet {if role == r {return true}}return false
}

这样的做法就可以满足我们对枚举更安全的需求了。


http://www.ppmy.cn/server/119720.html

相关文章

Weblogic中间件漏洞

一.后台弱⼝令GetShell 环境搭建 cd vulhub-master/weblogic/weak_password 15 docker-compose up -d 漏洞复现 默认账号密码&#xff1a;weblogic/Oracle123 weblogic 常⽤弱⼝令&#xff1a;https://cirt.net/passwords?criteriaweblogic 192.168.0.115/console/login/…

如何保护电脑文件夹?多种方法保护数据安全

在数字化时代&#xff0c;电脑中的文件夹往往存储着我们最私密、最重要的数据&#xff0c;如个人文档、照片、工作资料等。因此&#xff0c;保护这些文件夹免受未经授权的访问、盗窃或损坏变得尤为重要。本文将介绍多种方法来有效保护电脑文件夹及其数据安全。 文件夹保护3000 …

Docker Compose 启动 PostgreSQL 数据库

Docker Compose 启动 PostgreSQL 数据库 文章目录 Docker Compose 启动 PostgreSQL 数据库一 配置 docker-compose.pgsql.yml二 yml 配置说明三 启动容器四 停止容器 本文介绍了如何通过 Docker Compose 快速启动 PostgreSQL 数据库。在 docker-compose.pgsql.yml 文件中&…

SpringBoot 消息队列RabbitMQ在代码中声明 交换机 与 队列使用注解创建

创建Fanout交换机 Configuration public class FanoutConfig {Beanpublic FanoutExchange fanoutExchange(){return new FanoutExchange("csdn.fanout");//交换机名称} }创建队列 Beanpublic Queue fanoutQueue3(){return new Queue("csdn.queue");}绑定…

redis常见问题

为什么使用缓存&#xff1f; 高性能&#xff1a;优化实际执行&#xff0c;当数据缓存到redis中后&#xff0c;后续的请求&#xff0c;实际上不需要去查询DB&#xff0c;提高了查询性能。500ms —> 50ms高并发&#xff1a;redis支持10wqps&#xff0c;避免直接访问DB 使用缓存…

机器学习笔记(一)初识机器学习

1.定义 机器学习是一门多学科交叉专业&#xff0c;涵盖概率论知识&#xff0c;统计学知识&#xff0c;近似理论知识和复杂算法知识&#xff0c;使用计算机作为工具并致力于真实实时的模拟人类学习方式&#xff0c;并将现有内容进行知识结构划分来有效提高学习效率。 机器学习有…

制作炫酷个人网页:用 HTML 和 CSS3 展现你的风格

你是否觉得自己的网站应该看起来更炫酷&#xff1f;今天我将教你如何使用 HTML 和 CSS3 制作一个拥有炫酷动画和现代设计风格的个人网页&#xff0c;让它在任何设备上看起来都无敌酷炫&#xff01; 哈哈哈哈哈哈哈哈,我感觉自己有点中二哈哈哈哈~ 目录 炫酷设计理念构建 HTML …

51单片机应用开发---数码管的控制应用

实现目标 1、掌握数码管结构、驱动原理及应用&#xff1b; 2、掌握数码管段码表推导&#xff1b; 3、会编程让开发板8个数码管动态显示。 一、什么是数码管&#xff1f; 1.数码管定义 数码管&#xff0c;也称为LED数码管&#xff0c;基本单元是发光二极管(LED)。分为七段数…