字符串

  • 字符串的基础操作
  • 字符串的基础知识
  • 字符串的底层操作

字符串的基础操作

// 声明
var s = "你好"// s := "你好"
//读取
fmt.Print(s)
// 转化为unicode码点存储,单个字符, 例如'你',也是使用的rune来存储的,、
// 不过string底层本身是[]byte的方式存储的
_ = []rune(s)
// 转化为字符存储
_ = []byte(s)
// 字符的简单拼接
s1 := "你"
s2 := "好"
s := s1 + s2
//多行字符
s = `
你好

世界
`
// 多行字符也通常用在屏蔽 ""作用的地方
s= `{"user": "shgopher", "links": ["https://github.com/shgopher"]}`

字符串的基础知识

  • 字符串的数据是只读数据不可更改
  • 字符串的零值是可用的,var s string 结果是 ""
  • 获取字符串长度的操作时间复杂度是 O(1) 因为它是不可变的只读数据,所以长度被保存在了字段中,直接读这个字段即可
  • 字符串可以通过 ++= 进行拼接
  • 字符串可以使用 > < >= <= == =! 运算符,比较的顺序是:
    • 先比较长度
    • 再比较是否是指向一块内存地址
    • 如果都满足再比较具体数据
  • 字符串原生支持 unicode 字符集,并且 go 默认支持 utf-8 的编码算法
    • rune 存储 unicode 的一个码点
    • byte 存储真实的底层字符 (比如 utf-8,三个字符来保存一个中文字符,rune 就只显示一个字符,byte 会显示三个)
    package main
    
    import "fmt"
    
    func main() {
      var s = "中"
      fmt.Print([]byte(s), []rune(s))
    }
    // output: [228 184 173] [20013]
    
  • 使用``原生支持多行字符

字符串的高效构造

字符串的构造有以下这么几种

  • 最常规的使用 ++=
  • fmt.Sprintf
  • strings.Join
  • strings.Builder
  • bytes.Buffer

让我们分别给出代码:

//最常规的使用`+`和 `+=`
package main

import "fmt"

func main() {
	s1 := "中"
	s2 := "国"
	fmt.Print(s1 + s2)
}
//fmt.Sprintf
package main

import (
	"fmt"
)

func main() {
	s1 := "中"
	s2 := "国"
	fmt.Print(fmt.Sprintf("%s%s", s1, s2))
}
//strings.Join
package main

import (
	"fmt"
	"strings"
)

func main() {
	s1 := "中"
	s2 := "国"
	fmt.Print(strings.Join([]string{s1, s2}, ""))
}
//strings.Builder
package main

import (
	"fmt"
	"strings"
)

func main() {
	var b strings.Builder
  b.Grow(2) // 给出猜测最终的string长度
	s1 := "中"
	s2 := "国"
	b.WriteString(s1)
	b.WriteString(s2)
	fmt.Print(b.String())
}
//bytes.Buffer
package main

import (
	"bytes"
	"fmt"
)

func main() {
	var b bytes.Buffer
	s1 := "中"
	s2 := "国"
	b.WriteString(s1)
	b.WriteString(s2)
	fmt.Print(b.String())
}

根据 benchmark,得出以下结论:

  • 带有预估 string 长度的 strings.Builder 最快
  • 带有预估的 bytes.Buffer 和 strings.Join 性能第二档次
  • 没有预估长度的 strings.Builder 和 bytes.Buffer 以及 + += 第三档次
  • fmt.Sprintf 最差劲

那么:

  • 当能给出预估的情况下,优选使用 strings.Builder
  • strings.Joins 性能最稳,没有预估的情况下,使用这个稳定啊 (实际上这个 join 就是调用了 string.Builder,并且给出了预估长度)
func Join(elems []string, sep string) string {
switch len(elems) {
case 0:
	return ""
case 1:
	return elems[0]
}
// 这里是搞定string长度的
n := len(sep) * (len(elems) - 1)
for i := 0; i < len(elems); i++ {
	n += len(elems[i])
}
// 使用了builder
var b Builder
b.Grow(n)
b.WriteString(elems[0])
for _, s := range elems[1:] {
	b.WriteString(sep)
	b.WriteString(s)
}
return b.String()
}

  • 操作符 + += 最直观,并且在字符短,以及编译器知道连接的字符串个数时,这种方式还能得到编译器的优化
  • fmt.Sprintf 用在多类型组成字符串的时候是最好的,虽然它效率很差,但是人家能力强啊

综上所诉:优先选 strings.Join

字符串的底层

数据结构

一个 string 的底层数据类似一个 slice,只不过这个 slice 是只读数据,它的底层不同于一般的 slice,是一个特别的 struct

type stringStruct struct{
	str unsafe.Pointer
	len int 
	// 注意常规的slice这里是有一个cap的
	//但是string因为是只读的关系只有length的含义
}

runtime/string.go 中出现了这么一段代码

// rawstring allocates storage for a new string. The returned
// string and byte slice both refer to the same storage.
// The storage is not zeroed. Callers should use
// b to set the string contents and then drop b.
func rawstring(size int) (s string, b []byte) {
	p := mallocgc(uintptr(size), nil, false)

	stringStructOf(&s).str = p
	stringStructOf(&s).len = size

	*(*slice)(unsafe.Pointer(&b)) = slice{p, size, size}

	return
}

我们仔细看注释的这句话,当一个 string 导入数据的时候,运行时会给定一个辅助的 slice,用来辅助的导入数据,然后当数据导入完毕之后,这个 slice 的描述符,也就是这个代表了这个 slice 的 struct 就会被删除掉,所以说其实 string 不能跟 slice 划上等号,也不能简简单单的说它是一个只读的 slice,实际上它压根就不是 slice,slice 在生成 string 的过程中只是起到了辅助作用

类型转换

字符串进行的转化只能是 string 和 []rune or []byte 互相转换

package main

import "fmt"

func main() {
	a := "【你好】"
	b := []byte(a)
	c := []rune(a)
	// []byte 是可以直接使用fmt包直接输出为string的,但是[]rune需要显式进行转换。
	fmt.Printf("现在我们打印出原始数据%s,打印出[]byte转化后的数据%v,打印出[]rune转化后的数据%v,打印出逆转的数据%s 和 %s,", a, b, c, b, string(c))
}

上文我们提到的字符串的构造,例如删除一个字符,追加一个字符,都无一例外需要改变这个 string,那么很明显任何数据的处理都是拷贝的数据,原数据是不会有任何变化的,所以这就告诫我们字符串的处理要小心非常有可能浪费大量的内存。

我们看一下底层的转换代码:

const tmpStringBufSize = 32

type tmpBuf [tmpStringBufSize]byte

func stringtoslicebyte(buf *tmpBuf, s string) []byte {
	var b []byte
	if buf != nil && len(s) <= len(buf) {
		*buf = tmpBuf{}
		b = buf[:len(s)]
	} else {
		b = rawbyteslice(len(s))
	}
	// 这里就发生了拷贝
	copy(b, s)
	return b
}

于此同时我们也能发现 string 的底层的真实存储是 []byte 不是 []rune

package main

import "fmt"

func main() {
	a := "【你好】"
	var b []byte
	copy(b, a)
	var c []rune
	// invalid argument: arguments to copy c (variable of type []rune) 
	// and a (variable of type string) have different element types rune and byte
	copy(c, a)
	fmt.Println(b, c)
}

go 为某几种特别的情况优化了 string 和 slice 转换必须拷贝的情况,意思就是不需要拷贝就让这个 string 直接使用这个 slice 的底层,但是有个规定,只要是 slice 发生了改变,那么这个 string 立即失效

b = []int{1,2}

  • string(b) 用在 map 的 key 中 ma[string(b)]++
  • string(b) 在字符串的拼接句子中 “a” + string(b)
  • for-range 中的 string 到[]byte 的转换

for-range 字符串

因为对一个字符串使用 range 的时候,go 默认使用 utf8 的编码方式,但是 string 的底层是[]byte 的存储方式,所以直接 range 的时候,将这个时候的字符转化为字符就会发生乱码的情况

s := "你好"
for k := range s {
	// äå 
	fmt.Printf("%c",s[k])
}

解决方法有两种:

  1. 直接获取 value 值
func main() {
	s := "你好"
	// 这里的 v 就直接是 rune
	for _, v := range s {
		fmt.Printf("%c", v)
	}
}
  1. 将 s 转化为 []rune 来获取真正的 unicode 编码:
func main() {
	s := "你好"
	sr := []rune(s)
	// 这里的 v 就直接是 rune
	for i := range sr {
		fmt.Printf("%c", sr[i])
	}
}