จะสร้างสตริงแบบสุ่มที่มีความยาวคงที่ใน Go ได้อย่างไร


300

ฉันต้องการสตริงอักขระแบบสุ่มเท่านั้น (ตัวพิมพ์ใหญ่หรือตัวพิมพ์เล็ก) ไม่มีตัวเลขใน Go วิธีที่เร็วและง่ายที่สุดในการทำเช่นนี้คืออะไร?


2
@VinceEmigh: นี่คือหัวข้อเมตาที่พูดถึงคำถามพื้นฐาน meta.stackoverflow.com/q/274645/395461 โดยส่วนตัวฉันคิดว่าคำถามพื้นฐานนั้นใช้ได้ถ้าเขียนได้ดีและอยู่ในหัวข้อ ดูคำตอบด้านล่างพวกเขาแสดงสิ่งต่าง ๆ ที่จะเป็นประโยชน์สำหรับคนใหม่ที่จะไป สำหรับลูป, การพิมพ์แบบหล่อ, ทำ () ฯลฯ
แชนนอนแมตทิวส์

2
@Shannon " คำถามนี้ไม่ได้แสดงความพยายามในการวิจัยใด ๆ " (คำตอบ upvoted สูงครั้งแรกในลิงค์ของคุณ) - นั่นคือสิ่งที่ฉันหมายถึง เขาไม่แสดงความพยายามในการวิจัย ไม่มีความพยายามเลย (ความพยายามหรือแม้แต่ระบุว่าเขาดูออนไลน์ซึ่งเขาไม่ได้เห็นได้ชัด) แม้ว่ามันจะมีประโยชน์สำหรับคนใหม่เว็บไซต์นี้ไม่ได้เน้นการสอนคนใหม่ มันมุ่งเน้นไปที่การตอบปัญหาการเขียนโปรแกรม / คำถามไม่ใช่แบบฝึกหัด / คู่มือ แม้ว่ามันจะสามารถใช้สำหรับหลังนั่นไม่ใช่โฟกัสและดังนั้นคำถามนี้ควรถูกปิด แทนช้อนของมัน /:
วินซ์ Emigh

9
@VinceEmigh ฉันถามคำถามนี้ปีหลัง ฉันได้ค้นหาสตริงแบบสุ่มทางออนไลน์และอ่านเอกสารด้วย แต่มันก็ไม่เป็นประโยชน์ หากฉันยังไม่ได้เขียนในคำถามก็ไม่ได้หมายความว่าฉันไม่ได้ค้นคว้า
Anish Shah

คำตอบ:


809

ทางออกของ Paulให้วิธีแก้ปัญหาที่ง่ายและทั่วไป

คำถามที่ถามสำหรับ"วิธีที่เร็วและง่ายที่สุด" มาพูดถึงส่วนที่เร็วที่สุดด้วย เราจะมาถึงรหัสสุดท้ายและเร็วที่สุดในลักษณะวนซ้ำ การเปรียบเทียบการทำซ้ำแต่ละครั้งสามารถพบได้ในตอนท้ายของคำตอบ

การแก้ปัญหาและการเปรียบเทียบรหัสที่สามารถพบได้บนไปสนามเด็กเล่น รหัสในสนามเด็กเล่นเป็นไฟล์ทดสอบไม่ใช่ไฟล์เรียกทำงาน คุณต้องบันทึกลงในไฟล์ชื่อXX_test.goและเรียกใช้ด้วย

go test -bench . -benchmem

คำนำ :

วิธีแก้ปัญหาที่เร็วที่สุดไม่ใช่วิธีแก้ปัญหาหากคุณต้องการสตริงแบบสุ่ม สำหรับวิธีการแก้ปัญหาของพอลที่สมบูรณ์แบบ นี่คือถ้าประสิทธิภาพไม่สำคัญ แม้ว่า 2 ขั้นตอนแรก ( ไบต์และส่วนที่เหลือ ) อาจเป็นการประนีประนอมที่ยอมรับได้: พวกเขาปรับปรุงประสิทธิภาพโดยเช่น 50% (ดูตัวเลขที่แน่นอนในIIส่วนมาตรฐาน ) และพวกเขาไม่เพิ่มความซับซ้อนอย่างมีนัยสำคัญ

ต้องบอกว่าแม้ว่าคุณไม่ต้องการทางออกที่เร็วที่สุดการอ่านคำตอบนี้อาจเป็นการผจญภัยและการศึกษา

I. การปรับปรุง

1. เจเนซิส (รูน)

เพื่อเป็นการเตือนความทรงจำโซลูชันดั้งเดิมที่เราปรับปรุงอยู่คือ:

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. ไบต์

หากตัวอักษรให้เลือกและรวบรวมสตริงแบบสุ่มมีเพียงตัวอักษรตัวพิมพ์ใหญ่และตัวพิมพ์เล็กของตัวอักษรภาษาอังกฤษเราสามารถทำงานกับไบต์เท่านั้นเนื่องจากตัวอักษรภาษาอังกฤษจับคู่กับไบต์ 1 ต่อ 1 ในการเข้ารหัส UTF-8 (ซึ่ง คือวิธีที่ Go เก็บสตริง)

ดังนั้นแทนที่จะ:

var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")

เราสามารถใช้:

var letters = []bytes("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")

หรือดีกว่า:

const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"

ทีนี้นี่เป็นการปรับปรุงครั้งใหญ่: เราสามารถทำให้มันเป็นconst(มีstringค่าคงที่ แต่ไม่มีค่าคงที่แบบแบ่งย่อย ) ในฐานะที่เป็นกำไรพิเศษการแสดงออกlen(letters)จะเป็นconst! (นิพจน์len(s)เป็นค่าคงที่หากsเป็นค่าคงที่สตริง)

และราคาเท่าไหร่ ไม่มีอะไรทั้งนั้น. strings สามารถจัดทำดัชนีซึ่งดัชนีไบต์สมบูรณ์แบบสิ่งที่เราต้องการ

จุดหมายต่อไปของเราจะเป็นดังนี้:

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. ส่วนที่เหลือ

การแก้ปัญหาก่อนหน้านี้ได้รับการสุ่มหมายเลขที่กำหนดตัวอักษรแบบสุ่มโดยการเรียกrand.Intn()ซึ่งได้รับมอบหมายให้ซึ่งได้รับมอบหมายให้Rand.Intn()Rand.Int31n()

นี่จะช้ากว่าเมื่อเปรียบเทียบกับrand.Int63()ที่สร้างตัวเลขสุ่มด้วย 63 บิตสุ่ม

ดังนั้นเราสามารถเรียกrand.Int63()และใช้ส่วนที่เหลือได้หลังจากหารด้วยlen(letterBytes):

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()ทุกประการ แม้ว่าการบิดเบือนจะน้อยมากเนื่องจากจำนวนตัวอักษร52มีขนาดเล็กกว่า1<<63 - 1มากดังนั้นในทางปฏิบัติมันดีมาก

นี้เพื่อให้เข้าใจได้ง่ายขึ้น: 0..5สมมติว่าคุณต้องการจำนวนสุ่มในช่วงของ ใช้ 3 บิตสุ่มนี้จะผลิตตัวเลขที่มีความน่าจะเป็นสองเท่ากว่าจากช่วง0..1 2..5การใช้ 5 บิตสุ่มตัวเลขในช่วง0..1จะเกิดขึ้นกับ6/32ความน่าจะเป็นและตัวเลขในช่วงที่2..5มี5/32ความน่าจะเป็นซึ่งใกล้เคียงกับความต้องการ การเพิ่มจำนวนบิตทำให้สิ่งนี้มีความสำคัญน้อยลงเมื่อถึง 63 บิตมันมีความสำคัญน้อยมาก

4. กาว

เมื่อสร้างโซลูชันก่อนหน้านี้เราสามารถรักษาการกระจายตัวอักษรที่เท่ากันโดยใช้บิตสุ่มจำนวนต่ำสุดที่มากที่สุดเท่าที่จำเป็นเพื่อแสดงถึงจำนวนตัวอักษร ดังนั้นสำหรับตัวอย่างเช่นถ้าเรามี 52 ตัวอักษรก็ต้อง 6 52 = 110100bบิตเพื่อเป็นตัวแทนของมัน ดังนั้นเราจะใช้ต่ำสุด 6 rand.Int63()บิตจำนวนที่ส่งกลับโดย และเพื่อรักษากระจายเท่าเทียมกันของตัวอักษรเราเท่านั้น "ยอมรับ" 0..len(letterBytes)-1จำนวนถ้ามันตกอยู่ในช่วง หากบิตต่ำสุดสูงกว่าเราจะละทิ้งมันและสอบถามหมายเลขสุ่มใหม่

โปรดทราบว่าโอกาสของบิตต่ำสุดที่จะมากกว่าหรือเท่ากับlen(letterBytes)น้อยกว่า0.5โดยทั่วไป ( 0.25โดยเฉลี่ย) ซึ่งหมายความว่าแม้ว่าจะเป็นกรณีนี้การทำซ้ำกรณี "หายาก" นี้จะลดโอกาสที่จะไม่พบสิ่งที่ดี จำนวน. หลังจากการnทำซ้ำโอกาสที่เราจะไม่มีดัชนีที่ดีนั้นมีค่าน้อยกว่าpow(0.5, n)และนี่เป็นเพียงการประมาณค่าระดับสูง ในกรณีของ 52 ตัวอักษรโอกาสที่ 6 บิตต่ำสุดไม่ดีเป็นเพียง(64-52)/64 = 0.19; ซึ่งหมายความว่าเช่นว่าโอกาสที่จะได้มีตัวเลขที่ดีหลังจาก 10 1e-8ซ้ำเป็น

ดังนั้นนี่คือทางออก:

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. ปรับปรุงการกาว

วิธีการแก้ปัญหาก่อนหน้านี้เพียงใช้ต่ำสุด 6 บิต 63 rand.Int63()บิตแบบสุ่มกลับโดย นี่เป็นเรื่องเสียเพราะการได้บิตสุ่มเป็นส่วนที่ช้าที่สุดของอัลกอริทึมของเรา

หากเรามี 52 ตัวอักษรนั่นหมายถึง 6 บิตรหัสดัชนีตัวอักษร ดังนั้น 63 บิตสุ่มสามารถกำหนด63/6 = 10ดัชนีตัวอักษรที่แตกต่างกัน มาใช้ 10 ทั้งหมดนี้กัน:

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. ที่มา

การปรับปรุง Maskingค่อนข้างดีไม่มากที่เราสามารถปรับปรุงได้ เราทำได้ แต่ไม่คุ้มกับความซับซ้อน

ตอนนี้เราจะค้นหาสิ่งอื่นเพื่อปรับปรุง แหล่งที่มาของตัวเลขสุ่ม

มีcrypto/randแพ็คเกจที่มีRead(b []byte)ฟังก์ชั่นดังนั้นเราสามารถใช้มันเพื่อรับจำนวนมากด้วยการโทรเพียงครั้งเดียวเท่าที่เราต้องการ สิ่งนี้จะไม่ช่วยในเรื่องของประสิทธิภาพเนื่องจากcrypto/randใช้ตัวสร้างหมายเลขเทียมแบบเข้ารหัสลับที่ปลอดภัยดังนั้นมันจึงช้ากว่ามาก

งั้นลองทำmath/randแพ็คเกจกัน rand.Randใช้rand.Sourceเป็นแหล่งที่มาของบิตสุ่ม rand.Sourceเป็นอินเทอร์เฟซที่ระบุInt63() int64วิธีการ: สิ่งเดียวที่เราต้องการและใช้ในโซลูชันล่าสุดของเรา

ดังนั้นเราจึงไม่จำเป็นต้องมีrand.Rand(อย่างชัดเจนหรือทั่วโลกแบ่งปันหนึ่งในrandแพคเกจ) a rand.Sourceก็เพียงพอแล้วสำหรับเรา:

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)
}

โปรดทราบด้วยว่าโซลูชันล่าสุดนี้ไม่ต้องการให้คุณเริ่มต้น (เมล็ด) ทั่วโลกRandของmath/randแพคเกจเนื่องจากไม่ได้ใช้ (และเราrand.Sourceจะเริ่มต้น / เริ่มต้นอย่างเหมาะสม

อีกสิ่งที่ควรทราบที่นี่: package doc of math/randstates:

แหล่งที่มาเริ่มต้นมีความปลอดภัยสำหรับการใช้งานพร้อมกันหลาย goroutines

ดังนั้นแหล่งที่มาเริ่มต้นจะช้ากว่าSourceที่อาจจะได้รับโดยrand.NewSource()เพราะแหล่งที่มาเริ่มต้นที่มีการให้ความปลอดภัยภายใต้พร้อมกันการเข้าถึง / การใช้งานในขณะที่rand.NewSource()ไม่ได้นำเสนอนี้ (และทำให้Sourceกลับโดยจะมีแนวโน้มที่จะเร็ว)

7. การใช้ประโยชน์ strings.Builder

การแก้ปัญหาก่อนหน้านี้ทั้งหมดกลับstringมีเนื้อหาที่ถูกสร้างขึ้นเป็นครั้งแรกในชิ้น ( []runeในปฐมกาลและ[]byteในการแก้ไขปัญหาที่ตามมา) stringและแปลงแล้ว การแปลงครั้งสุดท้ายนี้จะต้องทำสำเนาเนื้อหาของชิ้นเพราะstringค่าไม่เปลี่ยนรูปและหากการแปลงจะไม่ทำสำเนาก็ไม่สามารถรับประกันได้ว่าเนื้อหาของสตริงจะไม่แก้ไขผ่านชิ้นเดิม ดูรายละเอียดได้ที่วิธีแปลงสตริง utf8 เป็น [] ไบต์ และgolang [] ไบต์ (สตริง) VS [] ไบต์ (* สตริง)

ไป 1.10 strings.Builderแนะนำ strings.Builderรูปแบบใหม่ที่เราสามารถใช้ในการสร้างเนื้อหาของการที่คล้ายกันstring bytes.Bufferมันใช้ภายใน[]byteและเมื่อเราทำเสร็จแล้วเราสามารถรับstringค่าสุดท้ายโดยใช้Builder.String()วิธีการของมัน แต่สิ่งที่เจ๋งคือมันทำสิ่งนี้โดยไม่ทำสำเนาที่เราเพิ่งพูดถึงข้างต้น มันกล้าที่จะทำเช่นนั้นเพราะไบต์ที่ใช้ในการสร้างเนื้อหาของสตริงไม่ถูกเปิดเผยดังนั้นจึงรับประกันได้ว่าไม่มีใครสามารถแก้ไขได้โดยไม่ตั้งใจหรือโดยเจตนาร้ายเพื่อเปลี่ยนสตริงที่

ดังนั้นความคิดต่อไปของเราคือไม่สร้างสตริงแบบสุ่มในชิ้น แต่ด้วยความช่วยเหลือของ a strings.Builderดังนั้นเมื่อเราเสร็จแล้วเราสามารถรับและส่งคืนผลลัพธ์โดยไม่ต้องคัดลอก สิ่งนี้อาจช่วยในเรื่องของความเร็วและแน่นอนจะช่วยในเรื่องของการใช้หน่วยความจำและการจัดสรร

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()
}

โปรดทราบว่าหลังจากสร้างใหม่strings.Buidlerแล้วเราเรียกBuilder.Grow()วิธีการของมันเพื่อให้แน่ใจว่าได้จัดสรรชิ้นส่วนภายในที่ใหญ่พอ (เพื่อหลีกเลี่ยงการจัดสรรใหม่เมื่อเราเพิ่มตัวอักษรแบบสุ่ม)

8. "การเลียนแบบ" strings.Builderพร้อมแพ็คเกจunsafe

strings.Builderสร้างสตริงในภายใน[]byteเช่นเดียวกับที่เราทำเอง โดยทั่วไปแล้วการทำมันผ่านทางstrings.Builderมีค่าใช้จ่ายบางอย่างสิ่งเดียวที่เราเปลี่ยนไปstrings.Builderคือการหลีกเลี่ยงการคัดลอกชิ้นสุดท้าย

strings.Builderหลีกเลี่ยงสำเนาสุดท้ายโดยใช้แพ็คเกจunsafe:

// String returns the accumulated string.
func (b *Builder) String() string {
    return *(*string)(unsafe.Pointer(&b.buf))
}

สิ่งคือเราสามารถทำสิ่งนี้เองได้เช่นกัน ดังนั้นความคิดที่นี่คือการสลับกลับไปที่การสร้างสตริงแบบสุ่มใน a []byteแต่เมื่อเราทำเสร็จแล้วอย่าแปลงเป็นstringกลับ แต่ทำการแปลงที่ไม่ปลอดภัย: รับstringจุดที่ชิ้นของไบต์เป็นข้อมูลสตริง .

นี่คือวิธีที่สามารถทำได้:

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. การใช้rand.Read())

ไป 1.7 เพิ่มrand.Read()ฟังก์ชั่นและRand.Read()วิธีการ เราควรถูกล่อลวงให้ใช้สิ่งเหล่านี้เพื่ออ่านจำนวนมากเท่าที่เราต้องการในขั้นตอนเดียวเพื่อให้ได้ประสิทธิภาพที่ดีขึ้น

มี "ปัญหา" เล็ก ๆ นี้: เราต้องการจำนวนไบต์กี่ไบต์? เราสามารถพูดได้ว่า: จำนวนตัวอักษรที่ส่งออก เราคิดว่านี่เป็นการประมาณค่าระดับสูงเนื่องจากดัชนีตัวอักษรใช้น้อยกว่า 8 บิต (1 ไบต์) แต่ ณ จุดนี้เรากำลังทำแย่ลงไปอีก (เนื่องจากการสุ่มบิตคือ "ส่วนที่ยาก") และเราได้รับมากกว่าที่ต้องการ

นอกจากนี้โปรดทราบว่าเพื่อรักษาการกระจายตัวของดัชนีจดหมายทั้งหมดอย่างเท่าเทียมกันอาจมีข้อมูลสุ่ม "ขยะ" ที่เราไม่สามารถใช้งานได้ดังนั้นเราจะสิ้นสุดการข้ามข้อมูลบางอย่างและจบลงเมื่อเราผ่านทั้งหมด ชิ้นไบต์ เราจะต้องได้รับเพิ่มเติมสุ่ม bytes "ซ้ำ" และตอนนี้เรากำลังสูญเสียความrandได้เปรียบ "การโทรหาแพ็คเกจเดียว" ...

เราสามารถ "ค่อนข้าง" math.Rand()เพิ่มประสิทธิภาพการใช้งานของข้อมูลแบบสุ่มที่เราได้รับจาก เราอาจประมาณจำนวนไบต์ (บิต) ที่เราต้องการ 1 ตัวอักษรต้องใช้letterIdxBitsบิตและเราต้องการnตัวอักษรดังนั้นเราจึงจำเป็นต้องมีn * letterIdxBits / 8.0ไบต์ปัดเศษขึ้น เราสามารถคำนวณความน่าจะเป็นของดัชนีแบบสุ่มที่ไม่สามารถใช้งานได้ (ดูด้านบน) ดังนั้นเราจึงสามารถร้องขอได้มากขึ้นว่า "มีโอกาสมากขึ้น" ก็เพียงพอแล้ว (หากปรากฎว่าไม่ใช่ เราสามารถประมวลผลชิ้นส่วนของไบต์เป็น "สตรีมบิต" ซึ่งเรามี lib บุคคลที่สามที่ดี: github.com/icza/bitio(เปิดเผย: ฉันเป็นผู้เขียน)

แต่รหัสเบนช์มาร์กยังคงแสดงว่าเราไม่ชนะ ทำไมถึงเป็นเช่นนั้น?

คำตอบสำหรับคำถามสุดท้ายคือเนื่องจากrand.Read()ใช้การวนซ้ำและทำการโทรSource.Int63()ต่อไปเรื่อย ๆ จนกว่ามันจะเติมชิ้นส่วนที่ส่งผ่าน สิ่งที่RandStringBytesMaskImprSrc()โซลูชันทำได้โดยไม่ต้องบัฟเฟอร์กลางและไม่มีความซับซ้อนเพิ่ม นั่นเป็นเหตุผลที่RandStringBytesMaskImprSrc()ยังคงอยู่บนบัลลังก์ ใช่RandStringBytesMaskImprSrc()ใช้สิ่งที่rand.Sourceไม่ซิงโครไนrand.Read()ซ์ แต่เหตุผลยังคงใช้; และสิ่งนี้พิสูจน์ได้ว่าเราใช้Rand.Read()แทนrand.Read()(แบบเดิมไม่ซิงโครไนซ์ด้วย)

ครั้งที่สอง เกณฑ์มาตรฐาน

เอาล่ะถึงเวลาสำหรับการเปรียบเทียบโซลูชันที่แตกต่างกันแล้ว

ช่วงเวลาของความจริง:

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

เพียงแค่เปลี่ยนจากอักษรรูนไบต์เราได้ทันทีมี24%กำไรจากผลการดำเนินงานและความต้องการหน่วยความจำลดลงถึงหนึ่งในสาม

การกำจัดrand.Intn()และการใช้งานrand.Int63()จะเพิ่มพลังให้อีก20%

การปิดบัง (และการทำซ้ำในกรณีของดัชนีขนาดใหญ่) ทำให้ช้าลงเล็กน้อย (เนื่องจากการโทรซ้ำ): -22% ...

แต่เมื่อเราทำให้การใช้งานทั้งหมด (หรือมากที่สุด) ของบิต 63 สุ่ม (10 ดัชนีจากที่หนึ่งrand.Int63()โทร): ความเร็วที่เพิ่มขึ้นครั้งใหญ่: 3 ครั้ง

หากเราชำระด้วย (ไม่ใช่ค่าเริ่มต้นใหม่) rand.Sourceแทนrand.Randเราจะได้รับ21%อีกครั้ง

ถ้าเราใช้strings.Builderเราได้รับเล็ก ๆ3.5%ในความเร็วแต่เรายังประสบความสำเร็จ50%ในการลดการใช้งานหน่วยความจำและการจัดสรร! เยี่ยมมาก!

สุดท้ายถ้าเรากล้าที่จะใช้แพคเกจunsafeแทนของstrings.Builderเราอีกครั้งได้รับความดี14%

เปรียบเทียบสุดท้ายเพื่อแก้ปัญหาเบื้องต้น: RandStringBytesMaskImprSrcUnsafe()เป็น6.3 เท่าเร็วกว่าRandStringRunes()ใช้หกหนึ่งหน่วยความจำและครึ่งหนึ่งเป็นไม่กี่จัดสรร ภารกิจเสร็จสมบูรณ์.


8
@RobbieV Yup เพราะที่ใช้ร่วมกันrand.Sourceถูกนำมาใช้ วิธีแก้ปัญหาที่ดีกว่าคือการส่งผ่านrand.SourceไปยังRandStringBytesMaskImprSrc()ฟังก์ชั่นและวิธีการไม่จำเป็นต้องล็อคและดังนั้นประสิทธิภาพ / ประสิทธิภาพจะไม่ได้รับผลกระทบ แต่ละ goroutine Sourceอาจจะมีเป็นของตัวเอง
icza

113
@icza นั่นเป็นหนึ่งในคำตอบที่ดีที่สุดที่ฉันเห็นมานานแล้วใน SO!
astropanic

1
@MikeAtlas: ควรหลีกเลี่ยงการใช้deferเมื่อเห็นได้ชัดว่าคุณไม่ต้องการมัน ดูgrokbase.com/t/gg/golang-nuts/158zz5p42w/…
Zan Lynx

1
@ZanLynx ขอบคุณสำหรับคำแนะนำ; แม้ว่าdeferการปลดล็อก mutex ทั้งก่อนหรือหลังการโทรล็อคเป็น IMO ส่วนใหญ่เป็นความคิดที่ดีมาก คุณรับประกันได้ว่าจะไม่ลืมที่จะปลดล็อค แต่ยังปลดล็อคแม้ในฟังก์ชั่นความตื่นตระหนกที่ไม่ร้ายแรง
Mike Atlas

1
@RobbieV ดูเหมือนว่ารหัสนี้จะเป็นเธรด / goroutine ที่ปลอดภัยเพราะแหล่งที่มาที่ใช้ร่วมกันนั้นเป็น LockedSource ซึ่งใช้ mutex ( golang.org/src/math/rand/rand.r:259 )
adityajones

130

คุณสามารถเขียนโค้ดได้ รหัสนี้อาจง่ายขึ้นเล็กน้อยถ้าคุณต้องการใช้ตัวอักษรทั้งหมดเป็นไบต์เดียวเมื่อเข้ารหัสใน UTF-8

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))
}

30
อย่าลืมเกี่ยวกับ rand.Seed () มิฉะนั้นคุณจะได้รับข้อความเดียวกันทุกครั้งที่เปิดตัวครั้งแรก ... rand.Seed (time.Now (). UTC (). UnixNano ())
Evan Lin

2
การเพิ่มของ Evan นั้นถูกต้อง แต่ก็มีตัวเลือกอื่นที่คล้ายคลึงกัน: rand.Seed(time.Now().Unix())หรือrand.Seed(time.Now().UnixNano())
openwonk

7
สำหรับความลับที่ยากต่อการคาดเดา - รหัสผ่านคีย์การเข้ารหัสลับ ฯลฯ .-- ไม่เคยใช้math/rand; ใช้crypto/rand(เช่นตัวเลือกของ @ Not_A_Golfer 1) แทน
twotwotwo

1
@EvanLin นี่จะเดาไม่ได้เหรอ? หากฉันต้องสร้างเครื่องกำเนิดไฟฟ้าจากนั้นผู้โจมตีสามารถคาดเดาเวลาที่ฉันเพาะและคาดการณ์ผลลัพธ์เดียวกันกับที่ฉันสร้าง
Matej

4
โปรดทราบว่าหากคุณลองใช้โปรแกรมด้านบนด้วยเมล็ดพันธุ์ในระหว่างเดินทางสนามเด็กเล่นจะได้ผลลัพธ์เหมือนเดิมตลอดเวลา ฉันลองเล่นที่สนามเด็กเล่นและหลังจากนั้นครู่หนึ่งก็รู้เรื่องนี้ มันทำงานได้ดีอย่างอื่นสำหรับฉัน หวังว่ามันจะช่วยประหยัดเวลา someones :)
Gaurav Sinha

18

ใช้แพ็คเกจuniuriซึ่งสร้างสตริงที่มีความปลอดภัย

คำเตือน: ฉันเป็นผู้เขียนของแพคเกจ


1
นอกเหนือจากนี้: ผู้เขียน, dchest เป็นผู้พัฒนาที่ยอดเยี่ยมและได้ผลิตแพ็คเกจขนาดเล็กที่มีประโยชน์เช่นนี้
Roshambo

16

สองตัวเลือกที่เป็นไปได้ (อาจมีมากกว่า):

  1. คุณสามารถใช้crypto/randแพคเกจที่สนับสนุนการอ่านอาร์เรย์แบบสุ่มไบต์ (จาก / dev / urandom) และมุ่งสู่การสร้างแบบเข้ารหัสลับ ดูhttp://golang.org/pkg/crypto/rand/#example_Read มันอาจจะช้ากว่าการสร้างตัวเลขสุ่มหลอก

  2. ใช้หมายเลขสุ่มแล้วแฮชด้วย md5 หรืออะไรทำนองนี้


4

ต่อไปนี้icza'sการแก้ปัญหาที่อธิบายไว้อย่างน่าพิศวงและนี่คือการเปลี่ยนแปลงของมันที่ใช้แทนcrypto/randmath/rand

const (
    letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" // 52 possibilities
    letterIdxBits = 6                    // 6 bits to represent 64 possibilities / indexes
    letterIdxMask = 1<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits
)

func SecureRandomAlphaString(length int) string {

    result := make([]byte, length)
    bufferSize := int(float64(length)*1.3)
    for i, j, randomBytes := 0, 0, []byte{}; i < length; j++ {
        if j%bufferSize == 0 {
            randomBytes = SecureRandomBytes(bufferSize)
        }
        if idx := int(randomBytes[j%length] & letterIdxMask); idx < len(letterBytes) {
            result[i] = letterBytes[idx]
            i++
        }
    }

    return string(result)
}

// SecureRandomBytes returns the requested number of bytes using crypto/rand
func SecureRandomBytes(length int) []byte {
    var randomBytes = make([]byte, length)
    _, err := rand.Read(randomBytes)
    if err != nil {
        log.Fatal("Unable to generate random bytes")
    }
    return randomBytes
}

หากคุณต้องการวิธีแก้ปัญหาทั่วไปที่ช่วยให้คุณสามารถส่งต่อในส่วนของอักขระไบต์เพื่อสร้างสตริงออกจากคุณสามารถลองใช้สิ่งนี้:

// SecureRandomString returns a string of the requested length,
// made from the byte characters provided (only ASCII allowed).
// Uses crypto/rand for security. Will panic if len(availableCharBytes) > 256.
func SecureRandomString(availableCharBytes string, length int) string {

    // Compute bitMask
    availableCharLength := len(availableCharBytes)
    if availableCharLength == 0 || availableCharLength > 256 {
        panic("availableCharBytes length must be greater than 0 and less than or equal to 256")
    }
    var bitLength byte
    var bitMask byte
    for bits := availableCharLength - 1; bits != 0; {
        bits = bits >> 1
        bitLength++
    }
    bitMask = 1<<bitLength - 1

    // Compute bufferSize
    bufferSize := length + length / 3

    // Create random string
    result := make([]byte, length)
    for i, j, randomBytes := 0, 0, []byte{}; i < length; j++ {
        if j%bufferSize == 0 {
            // Random byte buffer is empty, get a new one
            randomBytes = SecureRandomBytes(bufferSize)
        }
        // Mask bytes to get an index into the character slice
        if idx := int(randomBytes[j%length] & bitMask); idx < availableCharLength {
            result[i] = availableCharBytes[idx]
            i++
        }
    }

    return string(result)
}

หากคุณต้องการที่จะผ่านในแหล่งที่มาของตัวเองของแบบแผนก็จะเป็นที่น่ารำคาญในการปรับเปลี่ยนดังกล่าวข้างต้นจะยอมรับการแทนการใช้io.Readercrypto/rand


2

หากคุณต้องการตัวเลขสุ่มแบบเข้ารหัสที่ปลอดภัยและชุดอักขระที่แน่นอนมีความยืดหยุ่น (เช่น base64 ใช้ได้) คุณสามารถคำนวณความยาวของอักขระสุ่มที่คุณต้องการจากขนาดเอาต์พุตที่ต้องการ

ข้อความ 64 ฐานคือ 1/3 ยาวกว่าฐาน 256 (2 ^ 8 กับ 2 ^ 6; 8bits / 6bits = อัตราส่วน 1.333)

import (
    "crypto/rand"
    "encoding/base64"
    "math"
)

func randomBase64String(l int) string {
    buff := make([]byte, int(math.Round(float64(l)/float64(1.33333333333))))
    rand.Read(buff)
    str := base64.RawURLEncoding.EncodeToString(buff)
    return str[:l] // strip 1 extra character we get from odd length results
}

หมายเหตุ: คุณยังสามารถใช้ RawStdEncoding หากคุณต้องการ + และ / ตัวอักษรเพื่อ - และ _

หากคุณต้องการฐานสิบหกฐาน 16 นั้นยาวกว่าฐาน 256 2 เท่า (2 ^ 8 เทียบกับ 2 ^ 4; 8bits / 4bits = 2x อัตราส่วน)

import (
    "crypto/rand"
    "encoding/hex"
    "math"
)


func randomBase16String(l int) string {
    buff := make([]byte, int(math.Round(float64(l)/2)))
    rand.Read(buff)
    str := hex.EncodeToString(buff)
    return str[:l] // strip 1 extra character we get from odd length results
}

อย่างไรก็ตามคุณสามารถขยายไปยังชุดอักขระใดก็ได้ถ้าคุณมี base256 เป็น baseN encoder สำหรับชุดอักขระของคุณ คุณสามารถทำการคำนวณขนาดเดียวกันกับจำนวนบิตที่ต้องการเพื่อแสดงชุดอักขระของคุณ การคำนวณอัตราส่วน charset โดยพลการใด ๆ ที่เป็น: ratio = 8 / log2(len(charset)))

แม้ว่าวิธีแก้ปัญหาเหล่านี้ทั้งสองจะมีความปลอดภัยง่าย ๆ ควรรวดเร็วและไม่ทำให้เสียเอนโทรปีของ crypto

นี่คือสนามเด็กเล่นที่แสดงว่าใช้ได้กับทุกขนาด https://play.golang.org/p/i61WUVR8_3Z


ควรค่าแก่การกล่าวถึงว่า Go Playground จะส่งกลับตัวเลขสุ่มเสมอดังนั้นคุณจะไม่เห็นสตริงสุ่มที่แตกต่างกันในการประมวลผลรหัสที่ต่างกัน
TPPZ


1

นี่คือวิธีของฉัน) ใช้แรนด์คณิตศาสตร์หรือ crypto แรนด์ตามที่คุณต้องการ

func randStr(len int) string {
    buff := make([]byte, len)
    rand.Read(buff)
    str := base64.StdEncoding.EncodeToString(buff)
    // Base 64 can be longer than len
    return str[:len]
}

0

หากคุณยินดีที่จะเพิ่มอักขระบางตัวลงในกลุ่มอักขระที่อนุญาตคุณสามารถทำให้โค้ดทำงานกับทุกสิ่งที่มีการสุ่มไบต์ผ่าน io.Reader crypto/randที่นี่เราจะใช้

// len(encodeURL) == 64. This allows (x <= 265) x % 64 to have an even
// distribution.
const encodeURL = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"

// A helper function create and fill a slice of length n with characters from
// a-zA-Z0-9_-. It panics if there are any problems getting random bytes.
func RandAsciiBytes(n int) []byte {
    output := make([]byte, n)

    // We will take n bytes, one byte for each character of output.
    randomness := make([]byte, n)

    // read all random
    _, err := rand.Read(randomness)
    if err != nil {
        panic(err)
    }

    // fill output
    for pos := range output {
        // get random item
        random := uint8(randomness[pos])

        // random % 64
        randomPos := random % uint8(len(encodeURL))

        // put into output
        output[pos] = encodeURL[randomPos]
    }

    return output
}

ทำไมถึงrandom % 64จำเป็น?
Sung Cho

2
len(encodeURL) == 64เพราะ หากrandom % 64ไม่ได้ดำเนินการrandomPosอาจเป็น> = 64 และทำให้เกินขอบเขตที่ตื่นตระหนกขณะรันไทม์
0xcaff

-1
/*
    korzhao
*/

package rand

import (
    crand "crypto/rand"
    "math/rand"
    "sync"
    "time"
    "unsafe"
)

// 不全局共用rand库,减少锁竞争
type Rand struct {
    Seed int64
    Pool *sync.Pool
}

var (
    MRand    = NewRand()
    randlist = []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890")
)

// 初始化随机数发生器
func NewRand() *Rand {
    p := &sync.Pool{New: func() interface{} {
        return rand.New(rand.NewSource(getSeed()))
    },
    }
    mrand := &Rand{
        Pool: p,
    }
    return mrand
}

// 获取种子
func getSeed() int64 {
    return time.Now().UnixNano()
}

func (s *Rand) getrand() *rand.Rand {
    return s.Pool.Get().(*rand.Rand)
}
func (s *Rand) putrand(r *rand.Rand) {
    s.Pool.Put(r)
}

// 获取随机数
func (s *Rand) Intn(n int) int {
    r := s.getrand()
    defer s.putrand(r)

    return r.Intn(n)
}

// 批量获取随机数
func (s *Rand) Read(p []byte) (int, error) {
    r := s.getrand()
    defer s.putrand(r)

    return r.Read(p)
}

func CreateRandomString(len int) string {
    b := make([]byte, len)
    _, err := MRand.Read(b)
    if err != nil {
        return ""
    }
    for i := 0; i < len; i++ {
        b[i] = randlist[b[i]%(62)]
    }
    return *(*string)(unsafe.Pointer(&b))
}

24.0 ns / op 16 B / op 1 allocs /


สวัสดี! ยินดีต้อนรับสู่ StackOverflow แม้ว่าคุณจะเพิ่มข้อมูลโค้ด แต่คำตอบของคุณไม่ได้รวมบริบทใด ๆ เกี่ยวกับ "วิธีการทำงาน" หรือ "ทำไมจึงเป็นวิธีที่ทำได้" โปรดจำไว้ว่าคำถามนั้นถูกถามเป็นภาษาอังกฤษดังนั้นความคิดเห็นของคุณควรเป็นภาษาอังกฤษ
Cengiz Can

-2
const (
    chars       = "0123456789_abcdefghijkl-mnopqrstuvwxyz" //ABCDEFGHIJKLMNOPQRSTUVWXYZ
    charsLen    = len(chars)
    mask        = 1<<6 - 1
)

var rng = rand.NewSource(time.Now().UnixNano())

// RandStr 返回指定长度的随机字符串
func RandStr(ln int) string {
    /* chars 38个字符
     * rng.Int63() 每次产出64bit的随机数,每次我们使用6bit(2^6=64) 可以使用10次
     */
    buf := make([]byte, ln)
    for idx, cache, remain := ln-1, rng.Int63(), 10; idx >= 0; {
        if remain == 0 {
            cache, remain = rng.Int63(), 10
        }
        buf[idx] = chars[int(cache&mask)%charsLen]
        cache >>= 6
        remain--
        idx--
    }
    return *(*string)(unsafe.Pointer(&buf))
}

BenchmarkRandStr16-8 20000000 68.1 ns / op 16 B / op 1 allocs / op

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.