Bendi新闻
>
8种超简单的Golang生成随机字符串方式
8种超简单的Golang生成随机字符串方式
9月前
作者:华为云开发者联盟-张俭
链接:https://my.oschina.net/u/4526289/blog/10676267
前言
1. Runes
package approach1
import (
"fmt"
"math/rand"
"testing"
"time"
)
var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
func randStr(n int) string {
b := make([]rune, n)
for i := range b {
b[i] = letters[rand.Intn(len(letters))]
}
return string(b)
}
func TestApproach1(t *testing.T) {
rand.Seed(time.Now().UnixNano())
fmt.Println(randStr(10))
}
func BenchmarkApproach1(b *testing.B) {
rand.Seed(time.Now().UnixNano())
for i := 0; i < b.N; i++ {
_ = randStr(10)
}
}
var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
用这个替代
var letters = []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
或者更好
const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
len(letters)
也变为了一个常数(如果 s
为常数,那么 len(s)
也将是常数)letters
可以通过下标访问其中的 bytes 了,这正是我们需要的。package approach2
import (
"fmt"
"math/rand"
"testing"
"time"
)
const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
func randStr(n int) string {
b := make([]byte, n)
for i := range b {
b[i] = letters[rand.Intn(len(letters))]
}
return string(b)
}
func TestApproach2(t *testing.T) {
rand.Seed(time.Now().UnixNano())
fmt.Println(randStr(10))
}
func BenchmarkApproach2(b *testing.B) {
rand.Seed(time.Now().UnixNano())
for i := 0; i < b.N; i++ {
_ = randStr(10)
}
}
rand.Intn()
来获得一个随机字母,这个方法底层调用了 Rand.Intn()
,然后调用了 Rand.Int31n()
rand.Int63()
来说,Rand.Int31n()
很慢。rand.Int63()
然后除以 len(letterBytes)
,使用它的余数来生成字母package approach3
import (
"fmt"
"math/rand"
"testing"
"time"
)
const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
func randStr(n int) string {
b := make([]byte, n)
for i := range b {
b[i] = letters[rand.Int63() % int64(len(letters))]
}
return string(b)
}
func TestApproach3(t *testing.T) {
rand.Seed(time.Now().UnixNano())
fmt.Println(randStr(10))
}
func BenchmarkApproach3(b *testing.B) {
rand.Seed(time.Now().UnixNano())
for i := 0; i < b.N; i++ {
_ = randStr(10)
}
}
rand.Int63()
生成 63 比特的数字是等概率的)。由于字母总共才 52 个,远小于 1<<63 - 1,因此失真非常小,因此实际上这完全没问题。6/32
,2、3、4 出现的概率是 5/32
。现在接近了一些了,是吧?不断地增加比特位,这个差距就会变得越小,当你有 63 位地时候,这差别已经可忽略不计。4. Masking 掩码
rand.Int63()
的最后 6 位。并且,为了保持所有字符的均匀分布,我们决定只接受在 0..len(letterBytes)-1
的数字即 0~51。(译者注:这里已经没有第三个方案的不准确问题了)len(letterBytes)
的概率一般小于 0.5
(平均值为 0.25),这意味着出现这种情况,只要重试就好。重试 n 次之后,我们仍然需要丢弃这个数字的概率远小于 0.5 的 n 次方(这是上界了,实际会低于这个值)。以本文的 52 个字母为例,最低 6 位需要丢弃的概率只有 (64-52)/64=0.19
。这意味着,重复 10 次,仍然没有数字的概率是 1*10^-8。package approach4
import (
"fmt"
"math/rand"
"testing"
"time"
)
const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
const (
// 6 bits to represent a letters index
letterIdBits = 6
// All 1-bits as many as letterIdBits
letterIdMask = 1 <<letterIdBits - 1
)
func randStr(n int) string {
b := make([]byte, n)
for i := range b {
if idx := int(rand.Int63() & letterIdMask); idx < len(letters) {
b[i] = letters[idx]
i++
}
}
return string(b)
}
func TestApproach4(t *testing.T) {
rand.Seed(time.Now().UnixNano())
fmt.Println(randStr(10))
}
func BenchmarkApproach4(b *testing.B) {
rand.Seed(time.Now().UnixNano())
for i := 0; i < b.N; i++ {
_ = randStr(10)
}
}
rand.Int63()
方法返回的 64 个随机字节的后 6 位。这实在是太浪费了,因为 rand.Int63()
是我们算法中最耗时的部分了。63/6=10
次。rand.Int63()
方法返回的内容,使用 10 次,不过已经并不是协程安全的了。package approach5
import (
"fmt"
"math/rand"
"testing"
"time"
)
const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
const (
// 6 bits to represent a letter index
letterIdBits = 6
// All 1-bits as many as letterIdBits
letterIdMask = 1<<letterIdBits - 1
letterIdMax = 63 / letterIdBits
)
func randStr(n int) string {
b := make([]byte, n)
// A rand.Int63() generates 63 random bits, enough for letterIdMax letters!
for i, cache, remain := n-1, rand.Int63(), letterIdMax; i >= 0; {
if remain == 0 {
cache, remain = rand.Int63(), letterIdMax
}
if idx := int(cache & letterIdMask); idx < len(letters) {
b[i] = letters[idx]
i--
}
cache >>= letterIdBits
remain--
}
return string(b)
}
func TestApproach5(t *testing.T) {
rand.Seed(time.Now().UnixNano())
fmt.Println(randStr(10))
}
func BenchmarkApproach5(b *testing.B) {
rand.Seed(time.Now().UnixNano())
for i := 0; i < b.N; i++ {
_ = randStr(10)
}
}
crypto/rand
的包提供了 Read(b []byte)
的函数,可以通过这个函数获得需要的随机比特数,只需要一次调用。不过并不能提升性能,因为 crypto/rand
实现了一个密码学上的安全伪随机数,所以速度比较慢。math/rand
包,rand.Rand
使用 rand.Source
作为随机位的来源,rand.Source
是一个声明了 Int63() int64
的接口:正是我们在最新解决方案中需要和使用的唯一方法。rand.Rand
,rand.Source
包对于我们来说已经足够了package approach6
import (
"fmt"
"math/rand"
"testing"
"time"
)
const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
var src = rand.NewSource(time.Now().UnixNano())
const (
// 6 bits to represent a letter index
letterIdBits = 6
// All 1-bits as many as letterIdBits
letterIdMask = 1<<letterIdBits - 1
letterIdMax = 63 / letterIdBits
)
func randStr(n int) string {
b := make([]byte, n)
// A rand.Int63() generates 63 random bits, enough for letterIdMax letters!
for i, cache, remain := n-1, src.Int63(), letterIdMax; i >= 0; {
if remain == 0 {
cache, remain = src.Int63(), letterIdMax
}
if idx := int(cache & letterIdMask); idx < len(letters) {
b[i] = letters[idx]
i--
}
cache >>= letterIdBits
remain--
}
return string(b)
}
func TestApproach6(t *testing.T) {
fmt.Println(randStr(10))
}
func BenchmarkApproach6(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = randStr(10)
}
}
rand.Source
math/rand
的文档指出默认的Source是协程安全的
rand.NewSource()
创建出来的 Source
要慢。不用处理协程并发场景,当然慢啦。7. 使用 strings.Builder
[]byte
来构造内容,正是我们现在在做的,最后可以通过 Builder.String()
方法来获得最终的字符串值。但它很酷的地方在于,它无需执行刚才谈到的复制即可完成此操作。它敢这么做是因为它底层构造的 []byte
从未暴露出来,所以仍然可以保证没有人可以无意地、恶意地来修改已经生成的不可变字符串。package approach7
import (
"fmt"
"math/rand"
"strings"
"testing"
"time"
)
const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
var src = rand.NewSource(time.Now().UnixNano())
const (
// 6 bits to represent a letter index
letterIdBits = 6
// All 1-bits as many as letterIdBits
letterIdMask = 1<<letterIdBits - 1
letterIdMax = 63 / letterIdBits
)
func randStr(n int) string {
sb := strings.Builder{}
sb.Grow(n)
// A rand.Int63() generates 63 random bits, enough for letterIdMax letters!
for i, cache, remain := n-1, src.Int63(), letterIdMax; i >= 0; {
if remain == 0 {
cache, remain = src.Int63(), letterIdMax
}
if idx := int(cache & letterIdMask); idx < len(letters) {
sb.WriteByte(letters[idx])
i--
}
cache >>= letterIdBits
remain--
}
return sb.String()
}
func TestApproach7(t *testing.T) {
fmt.Println(randStr(10))
}
func BenchmarkApproach7(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = randStr(10)
}
}
Builder.Grow()
方法,使得它分配一个足够大的底层 slice, 避免在后续操作中再进行分配8. “Mimicing” strings.Builder with package unsafe
[]byte
来构建字符串。切换到 strings.Builder 可能有一些太重了,我们使用 strings.Builder 只是想避免拷贝 slice。unsafe
包来避免最终的拷贝// String returns the accumulated string.
func (b *Builder) String() string {
return *(*string)(unsafe.Pointer(&b.buf))
}
我们也可以自己完成这个流程。所以思路是我们通过 unsafe 包来返回一个字符串,来避免拷贝
package approach8
import (
"fmt"
"math/rand"
"testing"
"time"
"unsafe"
)
const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
var src = rand.NewSource(time.Now().UnixNano())
const (
// 6 bits to represent a letter index
letterIdBits = 6
// All 1-bits as many as letterIdBits
letterIdMask = 1<<letterIdBits - 1
letterIdMax = 63 / letterIdBits
)
func randStr(n int) string {
b := make([]byte, n)
// A rand.Int63() generates 63 random bits, enough for letterIdMax letters!
for i, cache, remain := n-1, src.Int63(), letterIdMax; i >= 0; {
if remain == 0 {
cache, remain = src.Int63(), letterIdMax
}
if idx := int(cache & letterIdMask); idx < len(letters) {
b[i] = letters[idx]
i--
}
cache >>= letterIdBits
remain--
}
return *(*string)(unsafe.Pointer(&b))
}
func TestApproach8(t *testing.T) {
fmt.Println(randStr(10))
}
func BenchmarkApproach8(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = randStr(10)
}
}
Benchmark
go test ./... -bench=. -benchmem
BenchmarkRunes-4 2000000 723 ns/op 96 B/op 2 allocs/op
BenchmarkBytes-4 3000000 550 ns/op 32 B/op 2 allocs/op
BenchmarkBytesRmndr-4 3000000 438 ns/op 32 B/op 2 allocs/op
BenchmarkBytesMask-4 3000000 534 ns/op 32 B/op 2 allocs/op
BenchmarkBytesMaskImpr-4 10000000 176 ns/op 32 B/op 2 allocs/op
BenchmarkBytesMaskImprSrc-4 10000000 139 ns/op 32 B/op 2 allocs/op
BenchmarkBytesMaskImprSrcSB-4 10000000 134 ns/op 16 B/op 1 allocs/op
BenchmarkBytesMaskImprSrcUnsafe-4 10000000 115 ns/op 16 B/op 1 allocs/op
译者测试的数据
BenchmarkApproach1-12 3849038 299.5 ns/op 64 B/op 2 allocs/op
BenchmarkApproach2-12 5545350 216.4 ns/op 32 B/op 2 allocs/op
BenchmarkApproach3-12 7003654 169.7 ns/op 32 B/op 2 allocs/op
BenchmarkApproach4-12 7164259 168.7 ns/op 32 B/op 2 allocs/op
BenchmarkApproach5-12 13205474 89.06 ns/op 32 B/op 2 allocs/op
BenchmarkApproach6-12 13665636 84.41 ns/op 32 B/op 2 allocs/op
BenchmarkApproach7-12 17213431 70.37 ns/op 16 B/op 1 allocs/op
BenchmarkApproach8-12 19756956 61.41 ns/op 16 B/op 1 allocs/op
仅仅只是把 rune 切换到 byte,就获得了性能的大幅度提升 (大于百分之 20)
使用
rand.Int63()
代替rand.Intn()
也获得大幅度提升 (大于百分之 20)使用 Masking 并没有提升性能,相反在原作者哪里,反而性能下降了
不过使用了一次
rand.Int63()
返回的全部字符后,性能提升了 3 倍使用
rand.Source
替代rand.Rand
,性能提升了 21%使用
strings.Builder
,我们在速度上提升了 3.5%,并且把原本 2 次的内存分配,降低到了一次!使用
unsafe
包来代替strings.Builder
,性能提升了 14%
往期推荐
知名游戏开发者云风宣布开源基于Lua的自研游戏引擎Ant Engine
这里有最新开源资讯、软件更新、技术干货等内容
点这里 ↓↓↓ 记得 关注✔ 标星⭐ 哦
微信扫码关注该文公众号作者
来源:OSC开源社区
相关新闻
今年缺德舅最具争议的8种食品, 你吃过吗?领导让员工离职的8种套路,50%的人第一种就扛不住留意!Costco不能退的8种商品,买之前请三思对健康最有益的8种果蔬皮!有你喜欢的吗?用最简单的方式做投资!一文教会你如何定投5/25/2024-5/31/2024 法拉盛新龍興:逛超市没有文案,只有快乐,而提升幸福感最简单的方式,当然又是逛超市啦!逼员工主动辞职的8种套路,50%的人第一种就扛不住移民美国,没有比这更快更简单的方式了废掉一个孩子最简单的方式刚刚,英国这种签证禁令已正式生效!PSW签证也受波及!留英8种方式和适合人群大盘点刚刚,英国这种签证禁令本月正式生效!PSW签证也受影响!留英8种方式总结快收藏录取超友好的8所美国大学!排名高、能保底、爱录国际生......保护医生免受暴力!这是5种有效的方式废掉一个人最隐蔽的4种方式,越早知道越受益人民日报推荐:情绪管理的9种方式(建议收藏)Sora、梦境与比喻——模拟世界的三种方式朋友圈超受欢迎的8个公众号,你关注了吗?丨荐号提前退休?以下是您的社会保障福利可能受到影响的 3 种方式Java连接kubernates集群最优雅的两种方式2024年移民加拿大的4种方式,其中第2种特别适合普通人消费电子通往AI时代的七种方式|亮马桥小纪严选2023年各地人均可支配收入公布!有的已超8万元结束乌克兰战争的三种方式王煜全公开课:预知未来的N种获利方式