前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >Golang 生成随机字符串的高级玩法!

Golang 生成随机字符串的高级玩法!

作者头像
七点一刻
发布于 2022-06-14 02:02:22
发布于 2022-06-14 02:02:22
3.3K00
代码可运行
举报
运行总次数:0
代码可运行

Golang 生成随机字符串的高级玩法!

如题:用 Golang 生成随机字符串(大小写字母组成),最快、最简单的实现方式是怎样的? 原文:How to generate a random string of a fixed length in Go?[1]

随机字符串嘛,rand就行了哦,这还不是信手拈来?

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
package main

import (
    "fmt"
    "time"
    "math/rand"
)

var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")

func randSeq(n int) string {
    b := make([]rune, n)
    for i := range b {
        b[i] = letters[rand.Intn(len(letters))]
    }
    return string(b)
}

func main() {
    rand.Seed(time.Now().UnixNano())

    fmt.Println(randSeq(10))
}

源码作者:Paul[2]

这个世界很简单,复杂的是人。总有那么一波人要搞个大新闻,他们玩的就是人群中的不一样!于是乎,就有了下面这位老哥的高赞回答。

I. Improvements

如果仅仅是生成随机字符串,最快的方案也可能不是首选的。就此而言,Paul 的实现是很完美的。但如果确实很看重性能的话,不显著提高复杂度的情况的前提下(只需前两步),我们还能把性能再提升 50%(参考压测)。

话虽如此,即便你不需要最快的实现方案,但读一读也是有教育意义的,挑战一下自己嘛(备好啤酒、花生米,让我们一起看看作者是如何一步步把我们逼到陆地神仙的 ^_^)。

1. Genesis(Runes)

先看看原始版本:

内心os:这才是简洁、易读的代码啊! 但是,为了把这个逼装下去,同时也出于膜拜的心理(手动狗头),我还真仔细看看,且看他如何出招!!!

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func init() {
    rand.Seed(time.Now().UnixNano())
}

var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")

func RandStringRunes(n int) string {
    b := make([]rune, n)
    for i := range b {
        b[i] = letterRunes[rand.Intn(len(letterRunes))]
    }
    return string(b)
}

2. Bytes

如果要生成的字符串只包括大小写字母的话,直接用 bytes 就行了。因为英文字母 UTF-8 编码映射到字节时是1对1的。

取而代之,直接用:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")

或者,常量更好:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"

现在已有较大改进了:const 取代 slice;另外,len(letters)也可以搞成常量。

因为,如果 s 是一个字符串常量的话,len(s) 也是一个常量。

经过一波改进后,代码长这样:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"

func RandStringBytes(n int) string {
    b := make([]byte, n)
    for i := range b {
        b[i] = letterBytes[rand.Intn(len(letterBytes))]
    }
    return string(b)
}

3. Remainder

上一个版本是用 rand.Intn() 生成随机字符的,阅读源码可以知道 rand.Intn() 会降级为 rand.Int31n,最终再降级为 rand.Int63

本着闹革命要彻底的原则,一步到位直接调用 rand.Int63

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func RandStringBytesRmndr(n int) string {
    b := make([]byte, n)
    for i := range b {
        b[i] = letterBytes[rand.Int63() % int64(len(letterBytes))]
    }
    return string(b)
}

本次改动在速度上提升很多,但也有缺点(这是一个严谨的数学问题 ^_^):所有字母的生成概率是不完全相等的(假设 rand.Int63 生成的 63-bit 的数有相同概率)。尽管失真很小,毕竟 52 (字符数)相对于 1<<63 - 1 而言很小,因此在实践中是完全没有问题的。

便于理解:假设随机生成一个数,范围 [0,5]。 如果用 3-bits 随机生成,那么范围 [0,1] 内数字的概率会是 [2,5] 的 2 倍(因为实际生成的数字为 [0,7],一般地 会再对 [6,7]取模变成 [0, 1]); 如果用 5-bits 随机生成,那么范围 [0,1] 内数字的概率是 6/32[2,5]的概率是 5/32。相对来说,两部分的概率更接近一些了。 随着 bits 的增加,这种差异变变得更小,当达到 63-bits 时基本就可以忽略不计了。

4. Masking

基于前文,为保证字母的均匀分布,我们可以用最少的 bits 来表示要生成的随机数。例如,对于 52 个字母可以用 6-bits 表示:52 = 110110b,因此,只取 rand.Int63() 的最低 6-bits 即可。为保证满足均匀分布,我们只取落在范围 [0,len(letterBytes)-1] 内的数,如果数还是比较大就再随机一个新的。

话说,每次生成的随机数大于等于 len(letterBytes)的概率一般是小于0.5(平均为0.25);在重复n次后,还没有找到合适数字的概率会比 power(0.5,n)(这里只是一个上限)小很多。以 52 个字母为例(6-bits表示),未找到的概率仅有 (64-52/64) = 0.19,意味着重复 10 次后几率会是 1e-8

实现如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
const (
    letterIdxBits = 6                    // 6 bits to represent a letter index
    letterIdxMask = 1<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits
)

func RandStringBytesMask(n int) string {
    b := make([]byte, n)
    for i := 0; i < n; {
        if idx := int(rand.Int63() & letterIdxMask); idx < len(letterBytes) {
            b[i] = letterBytes[idx]
            i++
        }
    }
    return string(b)
}

5. Masking Improved

上一个版本只使用了rand.Int63() 返回的 63-bits 随机数中的最低 6-bits,这是一种极大的浪费,因为随机数是本算法中最费性能的部分了。

再进一步,63-bits 可以分为 63/6 = 10 部分,我们全都给他用上:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
const (
    letterIdxBits = 6                    // 6 bits to represent a letter index
    letterIdxMask = 1<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits
    letterIdxMax  = 63 / letterIdxBits   // # of letter indices fitting in 63 bits
)

func RandStringBytesMaskImpr(n int) string {
    b := make([]byte, n)
    // A rand.Int63() generates 63 random bits, enough for letterIdxMax letters!
    for i, cache, remain := n-1, rand.Int63(), letterIdxMax; i >= 0; {
        if remain == 0 {
            cache, remain = rand.Int63(), letterIdxMax
        }
        if idx := int(cache & letterIdxMask); idx < len(letterBytes) {
            b[i] = letterBytes[idx]
            i--
        }
        cache >>= letterIdxBits
        remain--
    }

    return string(b)
}

6. Source

通过前面一波猛搞(Masking Improved),提升空间已经不多了。尽管有,也不值得去大费周章了。

但是,子曰:只要思想不滑坡,办法总比困难多。于是乎,我们换个方向,把目标转向了随机数生成部分。

crypto/rand包也能生成随机数,但是它更慢。回过头来,还是得搞一搞 math/rand包了。阅读源码可知在 rand.Rand内部包了个rand.Source,嘿嘿,花拳绣腿,要啥自行车,有rand.Source是够了呢:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
var src = rand.NewSource(time.Now().UnixNano())

func RandStringBytesMaskImprSrc(n int) string {
    b := make([]byte, n)
    // A src.Int63() generates 63 random bits, enough for letterIdxMax characters!
    for i, cache, remain := n-1, src.Int63(), letterIdxMax; i >= 0; {
        if remain == 0 {
            cache, remain = src.Int63(), letterIdxMax
        }
        if idx := int(cache & letterIdxMask); idx < len(letterBytes) {
            b[i] = letterBytes[idx]
            i--
        }
        cache >>= letterIdxBits
        remain--
    }

    return string(b)
}

math/rand源码有云:The default Source is safe for concurrent use by multiple goroutines. 因此,直接用rand.NewSource可能会快那么一丢丢。

7. Utilizing strings.Builder

前面的所有方法都会先返回一个[]byte再强转为string。这个转换会创建 slice 内容的一个副本,因为string的内容是不可更改的。

How to convert utf8 string to byte?[3] golang: []byte(string) vs []byte(*string)[4]

相比 bytes.Buffer[5] 而言, strings.Builder[6] 是一个好的替代方案,最终 Builder.String()[7]转为string时就不必再做一次 copy 了(是不是很酷?)。

言归正传,strings.Builder 搞起!这样做有两个好处:

•不必再 make([]byte, n);•最后返回 string 时也省去了 copy 操作;

这样做会不仅在提速上会有提升,在内存使用、分配方面也有不小的改善:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func RandStringBytesMaskImprSrcSB(n int) string {
    sb := strings.Builder{}
    sb.Grow(n)
    // A src.Int63() generates 63 random bits, enough for letterIdxMax characters!
    for i, cache, remain := n-1, src.Int63(), letterIdxMax; i >= 0; {
        if remain == 0 {
            cache, remain = src.Int63(), letterIdxMax
        }
        if idx := int(cache & letterIdxMask); idx < len(letterBytes) {
            sb.WriteByte(letterBytes[idx])
            i--
        }
        cache >>= letterIdxBits
        remain--
    }

    return sb.String()
}

8. "Mimicing" strings.Builder with package unsafe

我们使用strings.Builder 的目的仅仅是为了避免 []byte 的 copy 操作,能否把 strings.Builder 这部分的开销也省了呢?先看核心逻辑:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// String returns the accumulated string.
func (b *Builder) String() string {
    return *(*string)(unsafe.Pointer(&b.buf))
}

原来如此,嘿嘿,这个我也能搞。话说从头,还是用[]byte,最终返回 string时做个 unsafe 转换:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func RandStringBytesMaskImprSrcUnsafe(n int) string {
    b := make([]byte, n)
    // A src.Int63() generates 63 random bits, enough for letterIdxMax characters!
    for i, cache, remain := n-1, src.Int63(), letterIdxMax; i >= 0; {
        if remain == 0 {
            cache, remain = src.Int63(), letterIdxMax
        }
        if idx := int(cache & letterIdxMask); idx < len(letterBytes) {
            b[i] = letterBytes[idx]
            i--
        }
        cache >>= letterIdxBits
        remain--
    }

    return *(*string)(unsafe.Pointer(&b))
}

9. Using rand.Read()

Go 1.7[8] 新增了 rand.Read()[9] 方法,一次可以生成多个 bytes,是否可以提速呢?先说答案:不太行!

因为它的底层实现也是个循环(和 RandStringBytesMaskImprSrc 的做法是一样的):

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func read(p []byte, src Source, readVal *int64, readPos *int8) (n int, err error) {
    pos := *readPos
    val := *readVal
    rng, _ := src.(*rngSource)
    for n = 0; n < len(p); n++ {
        if pos == 0 {
            if rng != nil {
                val = rng.Int63()
            } else {
                val = src.Int63()
            }
            pos = 7
        }
        p[n] = byte(val)
        val >>= 8
        pos--
    }
    *readPos = pos
    *readVal = val
    return
}

没有了额外的 buffer 、没有了附加的复杂度,正是 RandStringBytesMaskImprSrc 能一马当先的原因。

II. Benchmark

下面是各种版本的压测数据:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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

•优化点 1:仅 bytes 替换 runes 这一个优化点,性能就提升了 24%,内存占用降为原来的 1/3•优化点 2:rand.Int63() 替换 rand.Intn() 又提升了20%•优化点 3:Masking 又倒回去了 -22%,主要是由于重复调用引起的(无效的循环)。•优化点 4:Masking Improved 使用 63-bits 拆分(1拆10)之后,成果斐然:速度又提升了3倍•优化点 5:rand.Source 替换 rand.Rand,提升 *21% *•优化点 6:使用 strings.Builder 虽然速度仅提升 3.5%,但是内存方面降了 50%•优化点 7:使用 unsafe 替换 strings.Builder,又提升了 21%

最终,RandStringBytesMaskImprSrcUnsafe比最初版本randstringgrunes快了 6.3倍。仅用了原 1/6 的内存和1/2的资源分配。

References

How to generate a random string of a fixed length in Go?[10]

References

[1] How to generate a random string of a fixed length in Go?: https://stackoverflow.com/questions/22892120/how-to-generate-a-random-string-of-a-fixed-length-in-go [2] Paul: https://stackoverflow.com/questions/22892120/how-to-generate-a-random-string-of-a-fixed-length-in-go/22892986#22892986 [3] How to convert utf8 string to byte?: https://stackoverflow.com/questions/41460750/how-to-convert-utf8-string-to-byte/41460993#41460993 [4] golang: []byte(string) vs []byte(*string): https://stackoverflow.com/questions/43470284/golang-bytestring-vs-bytestring/43470344#43470344 [5] bytes.Buffer: https://golang.org/pkg/bytes/#Buffer [6] strings.Builder: https://golang.org/pkg/strings/#Builder [7] Builder.String(): https://golang.org/pkg/strings/#Builder.String [8] Go 1.7: https://golang.org/doc/go1.7#math_rand [9] rand.Read(): https://golang.org/pkg/math/rand/#Read [10] How to generate a random string of a fixed length in Go?: https://stackoverflow.com/questions/22892120/how-to-generate-a-random-string-of-a-fixed-length-in-go

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-06-12,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 七点一刻的魔法书 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
一步步提升Go语言生成随机字符串的效率
假如我们要生成一个固定长度的随机字符串,包含大小写字母,没有数字,没有特殊字符串,那么我们怎么做呢?需要怎样优化,才会更简单,更高效?在最终的方案之前,我们看看最常见的写法是怎样的,然后是如何一步步演进到最终的高效率方案的。好吧,先看下最原始的方案。
飞雪无情
2020/02/10
1.9K1
[Golang] 生成随机字符串
这样生成的随机字符串是永久的同样的字符串。 下面,我演进了一个方案,同样是用的math/rand包里的方法来实现的
用户2353021
2020/05/12
8.3K0
【翻译】使用Go生成一个随机字符串(密码)
来源: Generate a random string (password) · YourBasic Go https://yourbasic.org/golang/generate-random-string/
Regan Yue
2023/03/30
1.2K0
【翻译】使用Go生成一个随机字符串(密码)
Golang-optimization「2」: 字符串
本系列的第二个部分,本文来谈谈程序员们喜闻乐见的string在Golang中有哪些值得注意的优化点
Kevinello
2022/11/17
3650
Golang-optimization「2」: 字符串
Golang 语言怎么高效使用字符串?
在 Golang 语言中,string 类型的值是只读的,不可以被修改。如果需要修改,通常的做法是对原字符串进行截取和拼接操作,从而生成一个新字符串,但是会涉及内存分配和数据拷贝,从而有性能开销。本文我们介绍在 Golang 语言中怎么高效使用字符串。
frank.
2021/04/16
1.9K0
白话 Golang pprof
有时,我们开发的 Golang 程序会出现性能上不去,CPU 使用率达到 100%,内存使用量过大,死锁等性能问题,我们该如何定位程序出现上诉问题的具体位置,来解决程序的到性能瓶颈呢?
恋喵大鲤鱼
2021/06/09
1K0
白话 Golang pprof
Go语言如何高效的进行字符串拼接(6种方式进行对比分析)
string是一个8位字节的集合,通常但不一定代表UTF-8编码的文本。string可以为空,但是不能为nil。string的值是不能改变的。
Golang梦工厂
2022/07/11
7200
Go语言如何高效的进行字符串拼接(6种方式进行对比分析)
Golang 生成随机数字、随机字符串
生成随机数字 func RandomInt(start int,end int) int{ rand.Seed(time.Now().UnixNano()) random:=rand.Intn(end-start) random = start + random return random } 生成随机字符串 func RandString(len int) string { r := rand.New(rand.NewSource(time.Now().UnixN
IT工作者
2022/03/09
2.6K0
go 生成随机字符串和获得定长字符串
挑战A.I.,赢百万奖金......了解更多详情>>> 随机字符串 //RandomStr 随机生成字符串 func RandomStr(length int) string { str := "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" bytes := []byte(str) result := []byte{} r := rand.New(rand.NewSource(time.Now().UnixNano
solate
2019/07/19
1.8K0
Go字符串拼接 ,那种性能最佳?
字符串是每一门编程语言必不可缺的数据类型,作为强大的Go语言也一样。在日常的开发工作中,对Go字符串的操作是必不可少的,但不同的操作方式,其性能也是不同的。本文将从五大操作方式来进行演示,到底哪一种操作方式最佳。
兔云小新LM
2023/02/28
3.1K0
Go字符串拼接 ,那种性能最佳?
golang 字符串拼接方法对比
执行 go test string_test.go -benchmem -bench=".*" 结果: BenchmarkFmtSprintf-4 2962962 400.6 ns/op 112 B/op 3 allocs/op BenchmarkAdd-4 6629833 207.7 ns/op 64 B/op 2 allocs/op BenchmarkStringsJoin-4 4255318 291.6 ns/op 112 B/op 3 allocs/op BenchmarkBuffer-4 2948402 368.3 ns/op 176 B/op 4 allocs/op BenchmarkBuilder-4 3149605 352.1 ns/op 160 B/op 4 allocs/op PASS ok command-line-arguments 8.219s 执行效率排序+>join>fmt.Sprintf>strings.Builder>bytes.Buffer
孤烟
2023/09/06
2370
对比Golang不同方式拼接字符串性能
在Golang的代码开发中,我们经常会用到字符串的拼接。Golang提供了不同的字符串拼接方式,性能也不尽相同。有时候在做性能优化的时候,往往会看到有些同学想当然的选择一些自认为性能比较高的方法。但是实际情况是否真的能提升性能呢?我们一起来看一下。
KunkkaWu
2023/05/09
6970
对比Golang不同方式拼接字符串性能
慎重使用默认随机函数
在看rpc源码的时候,看到产生随机数的方法是调用r= rand.New(rand.NewSource(time.Now().Unix())),而小编通常使用的都是rand.Intxx,这两者有什么不一样呢?好奇查看rand.Intxx的实现,发现它用到了锁。我们知道锁会引起性能问题,那为啥产生随机数要加锁呢?
数据小冰
2022/08/15
5390
慎重使用默认随机函数
2022-04-07:给定一个只由‘a‘和‘b‘组成的字符串str, str中“ab“和“ba“子串都可以消除, 消除之后剩下字符会重新靠在一起,继续出现可以消除的子串...
方法二:分别求a和b的个数,然后做差,谁多输出谁。这个方法是我另外想的,经过大量测试,准确无误。
福大大架构师每日一题
2022/04/07
3540
Go语言字符串高效拼接(二)
在上一篇关于字符串拼接的文章 Go语言字符串高效拼接(一) 中,我们演示的多种字符串拼接的方式,并且使用一个例子来测试了他们的性能,通过对比发现,我们觉得性能高的Builder并未发挥出其应该的性能,反而+号拼接,甚至strings.Join方法的性能更优越,那么这到底是什么原因呢?今天我们开始解开他们神秘的面纱,解开谜底。
飞雪无情
2018/12/06
6360
Golang高性能编程实践
一个例子,content-service 在压测的时候发现过一个问题: 旧逻辑为了简化编码,在进行协议转换前,会对某些字段做一个 DeepCopy,因为转换过程需要原始数据,但我们完全可以通过一些处理逻辑的调整,比如调整先后顺序等移除 DeepCopy。优化前后性能对比如下:
腾讯技术工程官方号
2023/09/15
7241
Golang高性能编程实践
Go语言字符串高效拼接(三)
在上一篇关于字符串拼接的文章Go语言字符串高效拼接(二) 中,我们终于为Builder拼接正名了,果真不负众望,尤其是拼接的字符串越来越多时,其性能的优越性更加明显。
飞雪无情
2018/12/12
1.1K0
再不Go就来不及了!Go高性能编程技法解读
导语 | 代码的稳健、可读和高效是我们每一个coder的共同追求。本文将结合Go语言特性,为书写高效的代码,力争从常用数据结构、内存管理两个方面给出相关建议。话不多说,让我们一起学习Go高性能编程的技法吧。 一、常用数据结构 (一)反射虽好,切莫贪杯 标准库reflect为Go语言提供了运行时动态获取对象的类型和值以及动态创建对象的能力。反射可以帮助抽象和简化代码,提高开发效率。 Go语言标准库以及很多开源软件中都使用了Go语言的反射能力,例如用于序列化和反序列化的json、ORM框架 gorm、xorm等
腾讯云开发者
2022/03/29
8470
我是如何实现Go性能5倍提升的?
代码的稳健、可读和高效是每一位 coder 的共同追求,写出更高效的代码不仅让自己爽、让 reviewer 赏心悦目,更能对业务带来实际的正面影响。本文将从实践及源码层面对 Go 的高性能编程进行解析,带你进入 Go 的高性能世界。
腾讯云开发者
2024/01/04
2.1K1
我是如何实现Go性能5倍提升的?
【初识Go】| Day5 字典、字符串
字典/哈希表是一种巧妙并且实用的数据字结构。它是一个无序的key/value对的集合,其中所有的key都是不同的,然后通过给定的key可以在常数时间复杂度内检索、更新或删除对应的value。
yussuy
2020/12/18
4020
【初识Go】| Day5 字典、字符串
相关推荐
一步步提升Go语言生成随机字符串的效率
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档