41

I want to replace a letter at a specific index in a string: aaaaaaa -> aaabaaa. Is there a built in way to do this? I wrote the following helper function to use in the mean time:

func main() {
    input := "aaaaaaa"
    output := replaceAtIndex(input, 'b', 3)
}

func replaceAtIndex(input string, replacement byte, index int) string {
    return strings.Join([]string{input[:index], string(replacement), input[index+1:]}, "")
}
0

5 Answers 5

51

Strings are immutable in Go, you have to convert it to runes then modify it then convert it back to a string.

@chendesheng's solution is semi-correct, except you can use rune instead of byte, that way it will work on unicode as well.

func replaceAtIndex(in string, r rune, i int) string {
    out := []rune(in)
    out[i] = r
    return string(out)
}

playground

Sign up to request clarification or add additional context in comments.

1 Comment

[]rune is much slower than []byte. If you know the string is ASCII you are incurring an unnecessary performance penalty by using []rune.
32

Both of the answers (OneOfOne and Denys Séguret) are correct. I just wanted to show the performance difference between them (which is really noticeable when the string is big).

It turns out that using str[:index] + string(replacement) + str[index+1:] is way faster.

So the benchmark:

package main
import (
    "testing"
)

func replaceAtIndex1(str string, replacement rune, index int) string {
    out := []rune(str)
    out[index] = replacement
    return string(out)
}

func replaceAtIndex2(str string, replacement rune, index int) string {
    return str[:index] + string(replacement) + str[index+1:]
}

func generateString(n int) string{
    s := ""
    for i := 0; i < n; i++{
        s += "a"
    }
    return s
}

func BenchmarkSmall1(b *testing.B) {
    n := 10
    str, index, replacement := generateString(n), n / 2, 'B'

    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        replaceAtIndex1(str, replacement, index)
    }
}

func BenchmarkSmall2(b *testing.B) {
    n := 10
    str, index, replacement := generateString(n), n / 2, 'B'

    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        replaceAtIndex2(str, replacement, index)
    }
}

func BenchmarkMedium1(b *testing.B) {
    n := 100
    str, index, replacement := generateString(n), n / 2, 'B'

    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        replaceAtIndex1(str, replacement, index)
    }
}

func BenchmarkMedium2(b *testing.B) {
    n := 100
    str, index, replacement := generateString(n), n / 2, 'B'

    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        replaceAtIndex2(str, replacement, index)
    }
}

func BenchmarkBig1(b *testing.B) {
    n := 10000
    str, index, replacement := generateString(n), n / 2, 'B'

    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        replaceAtIndex1(str, replacement, index)
    }
}

func BenchmarkBig2(b *testing.B) {
    n := 10000
    str, index, replacement := generateString(n), n / 2, 'B'

    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        replaceAtIndex2(str, replacement, index)
    }
}

func main(){}

shows the following results (thanks Thomasz for spotting a copypasting error):


BenchmarkSmall1-4   10000000           228 ns/op
BenchmarkSmall2-4   10000000           126 ns/op
BenchmarkMedium1-4    500000          2091 ns/op
BenchmarkMedium2-4  10000000           190 ns/op
BenchmarkBig1-4        10000        209232 ns/op
BenchmarkBig2-4       500000          3629 ns/op

4 Comments

This isn't a fair comparison. Slicing str without converting to rune is useless.
str[:index] like operate the byte, it's quite different with rune, you maybe corrupt a rune in that way.. For example, you can use "很好, hello world" as string.
The first method is about 3 times faster than second for small strings if you use []byte instead of []rune. See my benchmarks here: 1f6042.blogspot.com/2023/12/…
If you know that the string is ASCII you are incurring an unnecessary performance penalty by using []rune.
22

You can concatenate strings with the + operator :

return input[:index] + string(replacement) + input[index+1:]

Be careful that index isn't the index of a "letter" but of the byte.

3 Comments

is there any benefit in this over strings.Join ?
One here interested in the comparison with strings.Join
str[:index] like operate the byte, it's quite different with rune, you maybe corrupt a rune in that way.. For example, you can use "很好, hello world" as string.
7

Just for fun:

package main                                                                                                                                                                                                

import (                                                                                                                                                                                                    
    "fmt"                                                                                                                                                                                                   
    "reflect"                                                                                                                                                                                               
    "syscall"                                                                                                                                                                                               
    "unsafe"                                                                                                                                                                                                
)                                                                                                                                                                                                           

// We should do this because by default strings in Go are read-only.                                                                                                                                                                                                                                                                                                                                                                                                                  
func mprotect(ptr uintptr, w bool) {                                                                                                                                                                        
    // Need to avoid "EINVAL addr is not a valid pointer,
    // or not a multiple of PAGESIZE."                                                                                                                   
    start := ptr & ^(uintptr(syscall.Getpagesize() - 1))                                                                                                                                                    

    prot := syscall.PROT_READ                                                                                                                                                                               
    if w {                                                                                                                                                                                                  
        prot |= syscall.PROT_WRITE                                                                                                                                                                          
    }                                                                                                                                                                                                       

    _, _, err := syscall.Syscall(                                                                                                                                                                           
        syscall.SYS_MPROTECT,                                                                                                                                                                               
        start, uintptr(syscall.Getpagesize()),                                                                                                                                                              
        uintptr(prot),                                                                                                                                                                                      
    )                                                                                                                                                                                                       
    if err != 0 {                                                                                                                                                                                           
        panic(err.Error())                                                                                                                                                                                  
    }                                                                                                                                                                                                       
}                                                                                                                                                                                                           

// This function is very, very very very unsafe.                                                                                                                                                            
// Nowhere and never use it!                                                                                                                                                                                
func replaceAtIndex(s string, b byte, i int) {                                                                                                                                                              
    h := *(*reflect.StringHeader)(unsafe.Pointer(&s))                                                                                                                                                       

    mprotect(h.Data, true)                                                                                                                                                                                  
    defer mprotect(h.Data, false)                                                                                                                                                                           

    *(*byte)(unsafe.Pointer(h.Data + uintptr(i))) = b                                                                                                                                                       
}                                                                                                                                                                                                           

func main() {                                                                                                                                                                                               
    h := "Hello, playground"                                                                                                                                                                                
    replaceAtIndex(h, 'x', 0)                                                                                                                                                                               
    fmt.Println(h)                                                                                                                                                                                          
}

Never try to use it somewhere in your code. It's slower than any standard solutions or examples above and much more unsafe. =)

(It is not working in playground, because syscall is not defined there).

Comments

5

I took Salvador's benchmark and added strings.Join() to it. His answer remains correct - str[:index] + string(replacement) + str[index+1:] is the fastest option for large strings assuming you want your original string preserved. strings.Join() is pretty close for small strings and very very close for large strings. I also added tests with even larger strings to see if strings.Join() becomes faster at any point - it doesn't seem to.

I also, just for funsies, hacked together two other implementations with unsafe and reflect

  • One makes a copy, so it doesn't modify the original string, and has very interesting performance results - faster for small strings, much faster for medium strings, much much slower for large strings
  • The other just does a mutable string and is, unsurprisingly, drastically faster for all purposes and runs in constant time, with the caveat that your original string is modified.

I also had to modify generateString() so it would the largest strings would generate in a reasonable timeframe ;)

Anyway, the code:

package main
import (
    "reflect"
    "strings"
    "testing"
    "unsafe"
)

func replaceAtIndex1(str string, replacement rune, index int) string {
    out := []rune(str)
    out[index] = replacement
    return string(out)
}

func replaceAtIndex2(str string, replacement rune, index int) string {
    return str[:index] + string(replacement) + str[index+1:]
}

func replaceAtIndex3(str string, replacement rune, index int) string {
    return strings.Join([]string{str[:index], str[index + 1:]}, string(replacement))
}

func strToBytes(str string) []byte {
    string_header := (*reflect.StringHeader)(unsafe.Pointer(&str))
    bytes_header := &reflect.SliceHeader{
        Data : string_header.Data,
        Len : string_header.Len,
        Cap : string_header.Len,
    }
    return *(*[]byte)(unsafe.Pointer(bytes_header))
}

func strToBytesCopy(str string) []byte {
    bytes_unsafe := strToBytes(str)
    bytes := make([]byte, len(bytes_unsafe))
    copy(bytes, bytes_unsafe)
    return bytes
}

func bytesToStr(bytes []byte) string {
    bytes_header := (*reflect.SliceHeader)(unsafe.Pointer(&bytes))
    string_header := &reflect.StringHeader{
        Data : bytes_header.Data,
        Len : bytes_header.Len,
    }
    return *(*string)(unsafe.Pointer(string_header))
}

func replaceAtIndex4(str string, replacement rune, index int) string {
    bytes := strToBytesCopy(str)
    bytes[index] = byte(replacement)
    return bytesToStr(bytes)
}

func replaceAtIndex5(str string, replacement rune, index int) string {
    bytes := strToBytes(str)
    bytes[index] = byte(replacement)
    return bytesToStr(bytes)
}

func generateString(n int) string{
    var b strings.Builder
    b.Grow(n)
    for i := 0; i < n; i++ {
        b.WriteRune('a')
    }
    return b.String()
}

func BenchmarkSmall1(b *testing.B) {
    n := 10
    str, index, replacement := generateString(n), n / 2, 'B'

    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        replaceAtIndex1(str, replacement, index)
    }
}

func BenchmarkSmall2(b *testing.B) {
    n := 10
    str, index, replacement := generateString(n), n / 2, 'B'

    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        replaceAtIndex2(str, replacement, index)
    }
}

func BenchmarkSmall3(b *testing.B) {
    n := 10
    str, index, replacement := generateString(n), n / 2, 'B'

    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        replaceAtIndex3(str, replacement, index)
    }
}

func BenchmarkSmall4(b *testing.B) {
    n := 10
    str, index, replacement := generateString(n), n / 2, 'B'

    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        replaceAtIndex4(str, replacement, index)
    }
}

func BenchmarkSmall5(b *testing.B) {
    n := 10
    str, index, replacement := generateString(n), n / 2, 'B'

    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        replaceAtIndex5(str, replacement, index)
    }
}

func BenchmarkMedium1(b *testing.B) {
    n := 100
    str, index, replacement := generateString(n), n / 2, 'B'

    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        replaceAtIndex1(str, replacement, index)
    }
}

func BenchmarkMedium2(b *testing.B) {
    n := 100
    str, index, replacement := generateString(n), n / 2, 'B'

    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        replaceAtIndex2(str, replacement, index)
    }
}

func BenchmarkMedium3(b *testing.B) {
    n := 100
    str, index, replacement := generateString(n), n / 2, 'B'

    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        replaceAtIndex3(str, replacement, index)
    }
}

func BenchmarkMedium4(b *testing.B) {
    n := 100
    str, index, replacement := generateString(n), n / 2, 'B'

    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        replaceAtIndex4(str, replacement, index)
    }
}

func BenchmarkMedium5(b *testing.B) {
    n := 100
    str, index, replacement := generateString(n), n / 2, 'B'

    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        replaceAtIndex5(str, replacement, index)
    }
}

func BenchmarkBig1(b *testing.B) {
    n := 10000
    str, index, replacement := generateString(n), n / 2, 'B'

    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        replaceAtIndex1(str, replacement, index)
    }
}

func BenchmarkBig2(b *testing.B) {
    n := 10000
    str, index, replacement := generateString(n), n / 2, 'B'

    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        replaceAtIndex2(str, replacement, index)
    }
}

func BenchmarkBig3(b *testing.B) {
    n := 10000
    str, index, replacement := generateString(n), n / 2, 'B'

    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        replaceAtIndex3(str, replacement, index)
    }
}

func BenchmarkBig4(b *testing.B) {
    n := 10000
    str, index, replacement := generateString(n), n / 2, 'B'

    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        replaceAtIndex4(str, replacement, index)
    }
}

func BenchmarkBig5(b *testing.B) {
    n := 10000
    str, index, replacement := generateString(n), n / 2, 'B'

    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        replaceAtIndex5(str, replacement, index)
    }
}

func BenchmarkHuge2(b *testing.B) {
    n := 100000
    str, index, replacement := generateString(n), n / 2, 'B'

    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        replaceAtIndex2(str, replacement, index)
    }
}

func BenchmarkHuge3(b *testing.B) {
    n := 100000
    str, index, replacement := generateString(n), n / 2, 'B'

    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        replaceAtIndex3(str, replacement, index)
    }
}

func BenchmarkHuge4(b *testing.B) {
    n := 100000
    str, index, replacement := generateString(n), n / 2, 'B'

    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        replaceAtIndex4(str, replacement, index)
    }
}

func BenchmarkHuge5(b *testing.B) {
    n := 100000
    str, index, replacement := generateString(n), n / 2, 'B'

    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        replaceAtIndex5(str, replacement, index)
    }
}

func BenchmarkGargantuan2(b *testing.B) {
    n := 10000000
    str, index, replacement := generateString(n), n / 2, 'B'

    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        replaceAtIndex2(str, replacement, index)
    }
}

func BenchmarkGargantuan3(b *testing.B) {
    n := 10000000
    str, index, replacement := generateString(n), n / 2, 'B'

    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        replaceAtIndex3(str, replacement, index)
    }
}

func BenchmarkGargantuan4(b *testing.B) {
    n := 10000000
    str, index, replacement := generateString(n), n / 2, 'B'

    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        replaceAtIndex4(str, replacement, index)
    }
}

func BenchmarkGargantuan5(b *testing.B) {
    n := 10000000
    str, index, replacement := generateString(n), n / 2, 'B'

    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        replaceAtIndex5(str, replacement, index)
    }
}

func main(){}

And the results:

BenchmarkSmall1-8           20000000            99.9 ns/op
BenchmarkSmall2-8           50000000            29.5 ns/op
BenchmarkSmall3-8           20000000            58.1 ns/op
BenchmarkSmall4-8           50000000            32.0 ns/op
BenchmarkSmall5-8           1000000000           2.93 ns/op
BenchmarkMedium1-8           1000000          1034 ns/op
BenchmarkMedium2-8          20000000            68.4 ns/op
BenchmarkMedium3-8          20000000            78.8 ns/op
BenchmarkMedium4-8          30000000            49.3 ns/op
BenchmarkMedium5-8          1000000000           3.02 ns/op
BenchmarkBig1-8                20000         89557 ns/op
BenchmarkBig2-8              1000000          1204 ns/op
BenchmarkBig3-8              1000000          1257 ns/op
BenchmarkBig4-8              1000000          1200 ns/op
BenchmarkBig5-8             1000000000           2.93 ns/op
BenchmarkHuge2-8              200000         10260 ns/op
BenchmarkHuge3-8              200000          9908 ns/op
BenchmarkHuge4-8              100000         13628 ns/op
BenchmarkHuge5-8            1000000000           2.99 ns/op
BenchmarkGargantuan2-8          2000        822881 ns/op
BenchmarkGargantuan3-8          2000        807522 ns/op
BenchmarkGargantuan4-8          1000       2148387 ns/op
BenchmarkGargantuan5-8      1000000000           2.96 ns/op

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.