在Go中创建随机字符串(译文)| Go主题月

2,907 阅读7分钟

Jon Calhoun

在本文中,我们将介绍如何用 Go 创建一个允许我们生成任意长度的随机字符串的函数。为此,我们将编写一个简短的 rand 包,它包装 math/rand 包,并提供以下两个功能:

  • StringWithCharset() - 此函数将包含字符集和长度,并用该字符集生成随机字符串。
  • String() - 此函数只占用长度,并用设置为默认字符的字符串生成随机字符串。

这意味着我们将创建一个名为 rand 的自定义包,该包将利用 math/rand 包提供的功能来创建我们自己的函数,并屏蔽大部分实现细节。这样,我们剩下的代码不需要关心生成随机字符串的实现细节,而可以简单地调用函数,例如 rand.String(10) 得到一个随机字符串,其中有 10 个字符。

用相同的包名包装另一个包似乎很奇怪,但根据我的经验(以及其他人的经验),当你希望根据构建的内容包装包时,或者如果你想隔离与应用程序的其他部分无关的一些细节,这种模式会很好地工作。

如果你发现自己仍然在应用程序的其他部分使用 math/rand 包,那么可以考虑将其中的一些代码移动到新的 rand 包中,这样就更容易重用和隔离测试。

旁注:我曾经写过关于在 Go 中使用字符串的技巧列表,其中有生成随机字符串的部分,但是我觉得这个主题值得单独发表,因为它本身是一个非常常见的请求。

创建我们的 rand

我们要做的第一件事是创建一个目录来存储我们的新包。这将根据你的本地环境进行更改,但我建议在你正在工作的任何目录中创建一个名为 rand 的文件夹。完成此操作后,在新创建的目录中创建名为 strings.go 的文件。

rand 目录将存储我们正在创建的 rand 包的所有代码。目前,我们只有与字符串相关的函数,但是随着项目需求的发展,欢迎你随时间的推移添加到包中。

rand/strings.go 文件我们将存储所有与随机字符串相关的函数。现在这是我们包的全部,但是你可能会发现自己会随着时间的推移更新这个包,所以最好从这个开始考虑。

打开 rand/strings.go ,在这里,我们将首先编写一个 init() 函数,该函数 math/rand 包的种子。

package rand

import (
  "math/rand"
  "time"
)

var seededRand *rand.Rand = rand.New(
  rand.NewSource(time.Now().UnixNano()))

对于以前写过 Go 的人来说,前几行应该很熟悉 — 我们首先声明我们的包,再在后面导入一些说明我们将在代码中使用的包。

之后,我们声明一个名为 seedrand 的全局变量, 类型为 *rand.Rand, 这种类型拥有 math/rand 包中几乎所有可用的函数,但是我们能够隔离它,这样其他代码就不会影响我们的种子。这一点很重要,因为我们导入的另一段代码可能也会植入 math/rand 包,并导致我们所有的“随机”函数实际上不是那么随机。

例如,如果我们用 rand.Seed(time.Now().UnixNano()) 作为种子,然后另一个初始值设定项调用 rand.Seed(1),我们的种子会被重写,这绝对不是我们想要的。通过一个 rand.Rand 实例,我们能够防止这种情况发生在我们的随机数发生器。

我们使用 rand.New() 函数初始化此变量,需要一个 rand.Source 作为参数。源基本上只是一个对象,它帮助我们使用我们提供的种子获得随机分布的数字。

如果你选择不创建 rand.Rand 对象,而是使用 math/rand 包提供的方法,请注意默认种子值为 1,因此如果忘记对其进行种子设定,则会发现“随机”包在每次运行应用程序时都会生成相同的数字序列。这也意味着,如果另一个包总是在 math/rand 包添加另一个数字(如 42),那么每次重新启动应用程序时,也会得到类似的可预测结果。

要看到这一点,请尝试运行以下简单程序,而不为 math/rand 包设置种子。

package main

import (
  "fmt"
  "math/rand"
)

func main() {
  fmt.Println("1:", rand.Int())
  fmt.Println("2:", rand.Int())
  fmt.Println("3:", rand.Int())
}

你的输出和我的一样吗?

1: 5577006791947779410
2: 8674665223082153551
3: 6129484611666145821

虽然它可能并不总是这样,但计算机所做的一切都不是随机的,因此创建一个真正的随机数生成器是一个具有挑战性的问题。相反,我们可以做的是种子一个随机数发生器的值,将改变每次我们运行我们的程序。这将给我们一个生成过程,它将生成伪随机数,而且由于每次程序运行时种子都会发生变化,因此我们不必担心它会变得可预测。

如果你正在处理更敏感的代码,比如加密包,这可能不是最适合你的,但是我将假设,如果你正在构建一个你知道的足够多的加密包,可以进行此调用。

回到我们的代码,接下来我们要做的是创建 StringWithCharset() 函数。正如我们之前所说的,这将接受一个整数,它指示我们要生成的随机字符串的长度,以及我们要使用的字符集。

对于我们的字符集,我们将使用字符串变量。虽然您可以在代码中使用字节片之类的内容,但这已经足够好了,而且我发现在代码中创建字符串集更简单。写 charset := "abcABC123" 很容易做到。

综上所述,我们可以用下面的代码编写函数。我们将很快讨论实现细节,但现在来看一下代码。

func StringWithCharset(length int, charset string) string {
  b := make([]byte, length)
  for i := range b {
    b[i] = charset[seededRand.Intn(len(charset))]
  }
  return string(b)
}

在第一行中,我们声明了一个大小为 length 的字节片;在接下来的几行中,我们通过迭代字节片中的每个索引并将字符集中的一个随机字节插入字节片来构建字符串。

我们使用 seededRand.Intn() 方法。这个方法返回一个介于 0n - 1 之间的随机数,其中 n 是方法调用的输入,因此通过传入字符集的长度作为输入,它将返回一个表示我们应该在随机字符串中使用的字节索引的随机数。

最后,我们将字节片转换成字符串并返回它。

下一步是添加 String() 函数。此函数的工作方式与 StringWithCharset() 基本相同,只是它有一个默认字符集。我们真正需要做的不是重写所有的逻辑,而是定义这个字符集,并用这个字符集调用现有的 StringWithCharset() 函数。

从我们的字符集开始,我们将创建一个包含字符 a-za-z0-9 的字符集常量。

const charset = "abcdefghijklmnopqrstuvwxyz" +
  "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"

然后我们创建 Strings() 函数并在其中调用 StringWithCharset()

func String(length int) string {
  return StringWithCharset(length, charset)
}

好啦。以防万一,你的最终代码最终应该类似于下边这样哈~。

package rand

import (
  "math/rand"
  "time"
)

const charset = "abcdefghijklmnopqrstuvwxyz" +
  "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"

var seededRand *rand.Rand = rand.New(
  rand.NewSource(time.Now().UnixNano()))

func StringWithCharset(length int, charset string) string {
  b := make([]byte, length)
  for i := range b {
    b[i] = charset[seededRand.Intn(len(charset))]
  }
  return string(b)
}

func String(length int) string {
  return StringWithCharset(length, charset)
}

原文链接:www.calhoun.io/creating-ra…