Go中更安全的枚举

简介: Go中更安全的枚举

枚举是网络应用的一个重要部分。Go并不支持它们,但有一些方法可以模拟它们。


许多显而易见的解决方案都远非理想。下面是我们使用的一些想法,通过设计使枚举更加安全。


iota


Go让你用iota来使用枚举。


const (
  Guest = iota
  Member
  Moderator
  Admin
)


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


iota对于用位运算定义的参数也很有效。


const (
  Guest = 1 << iota // 1
  Member            // 2
  Moderator         // 4
  Admin             // 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会很乐意接受-142,即使没有相应的角色。


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


反模式:整数枚举

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


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


type Role uint
const (
  Guest Role = iota
  Member
  Moderator
  Admin
)


它看起来更好,但仍有可能传递任何任意的整数来代替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 = iota
  Guest
  Member
  Moderator
  Admin
)


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 string
const (
  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, nil
  case Member.slug:
    return Member, nil
  case Moderator.slug:
    return Moderator, nil
  case Admin.slug:
    return Admin, nil
  }
  return Unknown, errors.New("unknown role: " + s)
}


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


这种方法在你处理业务逻辑时是完美的。保持结构在内存中始终处于有效状态,使你的代码更容易操作和理解。检查枚举类型是否为空就足够了,而且你可以确定它是一个正确的值。


这种方法有一个潜在的问题。如果我们用的是全局性的常亮,这样的做法需要不断的赋值,如下:


roles.Guest = role.Admin


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


校验方法


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


type Role string
const (
  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
}


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

相关文章
|
存储 缓存 安全
Go 语言中的 Sync.Map 详解:并发安全的 Map 实现
`sync.Map` 是 Go 语言中用于并发安全操作的 Map 实现,适用于读多写少的场景。它通过两个底层 Map(`read` 和 `dirty`)实现读写分离,提供高效的读性能。主要方法包括 `Store`、`Load`、`Delete` 等。在大量写入时性能可能下降,需谨慎选择使用场景。
|
SQL 安全 Go
【Go语言专栏】Go语言中的安全审计与漏洞修复
【4月更文挑战第30天】本文介绍了Go语言中的安全审计和漏洞修复实践。安全审计包括代码审查、静态分析、运行时分析、渗透测试和专业服务,借助工具如`go vet`、`staticcheck`、`gosec`等。修复漏洞的方法涉及防止SQL注入、XSS攻击、CSRF、不安全反序列化等。遵循最小权限原则、输入验证等最佳实践,结合持续学习,可提升Go应用安全性。参考[Go安全工作组](https://github.com/golang/security)和[OWASP Top 10](https://owasp.org/www-project-top-ten/)深入学习。
579 0
|
安全 Go C语言
Go常量的定义和使用const,const特性“隐式重复前一个表达式”,以及iota枚举常量的使用
这篇文章介绍了Go语言中使用`const`定义常量的方法,包括常量的特性“隐式重复前一个表达式”,以及如何使用`iota`实现枚举常量的功能。
|
存储 安全 程序员
|
存储 安全 编译器
go语言中进行不安全的类型操作
【5月更文挑战第10天】Go语言中的`unsafe`包提供了一种不安全但强大的方式来处理类型转换和底层内存操作。包含两个文档用途的类型和八个函数,本文也比较了不同变量和结构体的大小与对齐系数,强调了字段顺序对内存分配的影响。
298 8
go语言中进行不安全的类型操作
|
NoSQL 安全 Go
Go 语言 mongox 库:简化操作、安全、高效、可扩展、BSON 构建
go mongox 是一个基于泛型的库,扩展了 MongoDB 的官方库。通过泛型技术,它实现了结构体与 MongoDB 集合的绑定,旨在提供类型安全和简化的数据操作。 go mongox 还引入链式调用,让文档操作更流畅,并且提供了丰富的 BSON 构建器和内置函数,简化了 BSON 数据的构建。 此外,它还支持插件化编程和内置多种钩子函数,为数据库操作前后的自定义逻辑提供灵活性,增强了应用的可扩展性和可维护性。
308 13
|
存储 安全 Go
【Go语言精进之路】构建高效Go程序:掌握变量、常量声明法则与iota在枚举中的奥秘
【Go语言精进之路】构建高效Go程序:掌握变量、常量声明法则与iota在枚举中的奥秘
240 2
|
存储 缓存 安全
Golang深入浅出之-Go语言中的并发安全容器:sync.Map与sync.Pool
Go语言中的`sync.Map`和`sync.Pool`是并发安全的容器。`sync.Map`提供并发安全的键值对存储,适合快速读取和少写入的情况。注意不要直接遍历Map,应使用`Range`方法。`sync.Pool`是对象池,用于缓存可重用对象,减少内存分配。使用时需注意对象生命周期管理和容量控制。在多goroutine环境下,这两个容器能提高性能和稳定性,但需根据场景谨慎使用,避免不当操作导致的问题。
777 7
|
安全 Go
Golang深入浅出之-Go语言中的并发安全队列:实现与应用
【5月更文挑战第3天】本文探讨了Go语言中的并发安全队列,它是构建高性能并发系统的基础。文章介绍了两种实现方法:1) 使用`sync.Mutex`保护的简单队列,通过加锁解锁确保数据一致性;2) 使用通道(Channel)实现无锁队列,天生并发安全。同时,文中列举了并发编程中常见的死锁、数据竞争和通道阻塞问题,并给出了避免这些问题的策略,如明确锁边界、使用带缓冲通道、优雅处理关闭以及利用Go标准库。
778 5
|
安全 Go 开发工具
对象存储OSS产品常见问题之go语言SDK client 和 bucket 并发安全如何解决
对象存储OSS是基于互联网的数据存储服务模式,让用户可以安全、可靠地存储大量非结构化数据,如图片、音频、视频、文档等任意类型文件,并通过简单的基于HTTP/HTTPS协议的RESTful API接口进行访问和管理。本帖梳理了用户在实际使用中可能遇到的各种常见问题,涵盖了基础操作、性能优化、安全设置、费用管理、数据备份与恢复、跨区域同步、API接口调用等多个方面。
499 9