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
Go语言封装艺术:代码安全与结构清晰
Go语言封装艺术:代码安全与结构清晰
82 0
|
2月前
|
安全 Go C语言
Go常量的定义和使用const,const特性“隐式重复前一个表达式”,以及iota枚举常量的使用
这篇文章介绍了Go语言中使用`const`定义常量的方法,包括常量的特性“隐式重复前一个表达式”,以及如何使用`iota`实现枚举常量的功能。
|
6月前
|
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/)深入学习。
161 0
|
3月前
|
存储 安全 程序员
|
6月前
|
存储 安全 编译器
go语言中进行不安全的类型操作
【5月更文挑战第10天】Go语言中的`unsafe`包提供了一种不安全但强大的方式来处理类型转换和底层内存操作。包含两个文档用途的类型和八个函数,本文也比较了不同变量和结构体的大小与对齐系数,强调了字段顺序对内存分配的影响。
124 8
go语言中进行不安全的类型操作
|
5月前
|
存储 安全 Go
【Go语言精进之路】构建高效Go程序:掌握变量、常量声明法则与iota在枚举中的奥秘
【Go语言精进之路】构建高效Go程序:掌握变量、常量声明法则与iota在枚举中的奥秘
66 2
|
4月前
|
安全 Go
Go语言map并发安全,互斥锁和读写锁谁更优?
Go并发编程中,`sync.Mutex`提供独占访问,适合读写操作均衡或写操作频繁的场景;`sync.RWMutex`允许多个读取者并行,适用于读多写少的情况。明智选择锁可提升程序性能和稳定性。示例展示了如何在操作map时使用这两种锁。
58 0
|
4月前
|
安全 Go 开发者
Go语言map并发安全使用的正确姿势
在Go并发编程中,由于普通map不是线程安全的,多goroutine访问可能导致数据竞态。为保证安全,可使用`sync.Mutex`封装map或使用从Go 1.9开始提供的`sync.Map`。前者通过加锁手动同步,后者内置并发控制,适用于多goroutine共享。选择哪种取决于具体场景和性能需求。
60 0
|
4月前
|
存储 安全 Java
Go语言中的map为什么默认不是并发安全的?
Go语言的map默认不保证并发安全,以优化性能和简洁性。官方建议在需要时使用`sync.Mutex`保证安全。从Go 1.6起,并发读写map会导致程序崩溃,鼓励开发者显式处理并发问题。这样做的哲学是让代码更清晰,并避免不必要的性能开销。
56 0
|
5月前
|
NoSQL 安全 Go
Go 语言 mongox 库:简化操作、安全、高效、可扩展、BSON 构建
go mongox 是一个基于泛型的库,扩展了 MongoDB 的官方库。通过泛型技术,它实现了结构体与 MongoDB 集合的绑定,旨在提供类型安全和简化的数据操作。 go mongox 还引入链式调用,让文档操作更流畅,并且提供了丰富的 BSON 构建器和内置函数,简化了 BSON 数据的构建。 此外,它还支持插件化编程和内置多种钩子函数,为数据库操作前后的自定义逻辑提供灵活性,增强了应用的可扩展性和可维护性。
97 6