ausheng

Golang中的字符串

Golang 中的string类型存储的字符串是不可变的, 如果要修改string内容需要将string转换为[]byte或[]rune,并且修改后的string内容是重新分配的, 原字符串将被gc回收;

package main

import (
    "fmt"
)

func main() {
    s := "hi, go"
    fmt.Printf("value of str: %v\n", s)
    fmt.Printf("ptr of str: %p\n", &s)
    // 修改, 将,修改为!
    bs := []byte(str)
    bs[2] = '!' 
    fmt.Printf("value of lstr: %v\n", string(bs))
    fmt.Printf("ptr of lstr: %p\n", &bs)
}

结果:

value of str: hi, go
ptr of str: 0xc00000e1f0
value of lstr: hi! go
ptr of lstr: 0xc00000a080

可以看到bs 和 s 的地址空间不同了,可见字符串的修改是会重新分配的;

Golang中string有2种类型, 只包含ASCII码的string, 已经包含中文等其他复杂类型的string; 我们知道中文是占3个字节的;

其中:只包含ASCII码的string的string 能通过索引的方式查找对应位置的字符;而包含中文的string类型rune,要想完整的显示中文,需要使用for…range循环;

Golang字符串拼接方法

golang中要实现字符串的拼接,有很多种方法,最常见的当然是使用运算符”+”进行拼接了,还有很多其他的方法,下面依次介绍,并说明其优缺点.

直接使用运算符

// 不换行 
str := "hello, " + "golang" 

// 换行,换行是"+" 必须在上一行的结尾处 
str1 := "The only person " + 
                              "standing in your way " + 
                              "is you"

上面提到golang里面的字符串都是不可变的,每次运算都会产生一个新的字符串.

所以使用运算符”+”连接字符串会产生很多临时的无用的字符串,会给 gc 带来额外的负担,所以性能比较差.


使用fmt.Sprintf

fmt包是golang中的基础包,fmt 包实现了格式化I/O函数,类似于C的 printf 和 scanf.

在fmt包中提供了一个方法func Sprintf(format string, a ...interface{}) string,使用格式话的方式可以将多个字符串拼接到一起

str := fmt.Sprintf("%s %s %s", "format", "string", "by fmt.Sprintf")

这种方式,使用简单,虽然不会像”+”连接那样生成多余的string,但是内部实现颇为复杂,性能不是很好.

使用strings.Join

golang的 strings 包为字符串的拼接提供了一个方法func Join(a []string, sep string) string, Join的内部实现比fmt.Sprintf要简单的多,思路就是: Join会先根据字符串数组的内容,计算出一个拼接之后的长度,然后申请对应大小的内存,一个一个字符串填入.代码如下:

// Join 将传如的字符串连接成一个字符串
func Join(a []string, sep string) string {
  	// 如果字符串数量少,直接使用运算符拼接
    switch len(a) {
    case 0:
      return ""
    case 1:
      return a[0] 
    case 2:
      return a[0] + sep + a[1]
    case 3:
      return a[0] + sep + a[1] + sep + a[2]
    }

    // 计算最终字符串的字符大小
    // 首先计算连接符sep的大小
    n := len(sep) * (len(a) -1)

    // 计算被连接的字符串的字符数
    for _, value := range a {
      n += len(value)
    }

    // 知道了总的字符数量,创建对应大小的数组
    b := make([]byte, n)
    bp := copy(b, a[0])

    for _, s := range a[1:] {
      bp += copy(b[bp:], sep)
      bp += copy(b[bp:], s)
    }

    return string(b)
}

这种方式实现字符串的拼接,简单方便,效率也是很高的,建议使用,唯一的不足就是在生成数组的时候开销比较大;

使用bytes.Buffer

bytes包中的Buffer提供了一个方法 func (b *Buffer) WriteString(s string) (n int, err error)

WriteString将s的内容追加到缓冲区,并根据需要增加缓冲区。返回值n为s的长度;err总是nil。

如果缓冲区太大,WriteString将会因为ErrTooLarge而陷入恐慌。

package main

import (
    "fmt"
    "bytes"
)

func main() {
    // 声明一个Buffer
    var buf bytes.Buffer
    buf.WriteString("good ")
    buf.WriteString("boy!")
    fmt.Println(buf.String()) // good boy!
}

这个比较理想,可以当成可变字符使用,对内存的增长也有优化,如果能预估字符串的长度,还可以用 buffer.Grow() 接口来设置 capacity。

使用strings.Builder

strings.Builder 内部通过 slice 来保存和管理内容。slice 内部则是通过一个指针指向实际保存内容的数组。

strings.Builder 同样也提供了 Grow() 来支持预定义容量。

当我们可以预定义我们需要使用的容量时,strings.Builder 就能避免扩容而创建新的 slice 了。strings.Builder是非线程安全,性能上和 bytes.Buffer 相差无几。

package main

import (
    "fmt"
    "strings"
)

func main() {
    // 声明一个Buffer
    var buf strings.Builder
    buf.WriteString("good ")
    buf.WriteString("boy!")
    fmt.Println(buf.String()) // good boy!
}

ausheng

爱生活,爱编程

AUSHENG – BLOG

不要害羞,保持联系。我们喜欢结识有趣的人,结交新朋友。