Skip to content

Go字符串

在 Go 中,字符串本质上是一个不可变的、只读的字节序列(byte sequence),这里“字节序列”指的是字符串的底层数据由一串按顺序排列的字节组成,这些字节占用一片连续的内存空间。

字面量

前面提到过字符串有两种字面量表达方式,分为普通字符串和原生字符串。

普通字符串

普通字符串由""双引号表示,支持转义,不支持多行书写,下列是一些普通字符串

go
"这是一个普通字符串\n"
"abcdefghijlmn\nopqrst\t\\uvwxyz"
这是一个普通字符串
abcdefghijlmn
opqrst  \uvwxyz

原生字符串

原生字符串由反引号表示,不支持转义,支持多行书写,原生字符串里面所有的字符都会原封不动的输出,包括换行和缩进。

go
`这是一个原生字符串,换行
  tab缩进,\t制表符但是无效,换行
  "这是一个普通字符串"

  结束
`
这是一个原生字符串,换行
        tab缩进,\t制表符但是无效,换行
        "这是一个普通字符串"

        结束

访问

因为字符串本质是字节序列,其索引操作str[i]被设计为返回第 i 个字节,语法上与切片一致,例如访问字符串第一个元素

go
func main() {
   str := "this is a string"
   fmt.Println(str[0])
}

输出是字节编码值而不是字符

116

切割字符串

go
func main() {
   str := "this is a string"
   fmt.Println(string(str[0:4]))
}
this

尝试修改字符串元素

go
func main() {
   str := "this is a string"
   str[0] = 'a' // 无法通过编译
   fmt.Println(str)
}
main.go:7:2: cannot assign to str[0] (value of type byte)

虽然没法修改字符串,但是可以覆盖

go
func main() {
   str := "this is a string"
   str = "that is a string"
   fmt.Println(str)
}
that is a string

转换

字符串可以转换为字节切片,而字节切片或字节序列也可以转换为字符串,例子如下:

go
func main() {
   str := "this is a string"
   // 显式类型转换为字节切片
   bytes := []byte(str)
   fmt.Println(bytes)
   // 显式类型转换为字符串
   fmt.Println(string(bytes))
}

字符串的内容是只读的不可变的,无法修改,但是字节切片是可以修改的。

go
func main() {
  str := "this is a string"
  fmt.Println(&str)
  bytes := []byte(str)
    // 修改字节切片
  bytes = append(bytes, 96, 97, 98, 99)
    // 赋值给原字符串
  str = string(bytes)
  fmt.Println(str)
}

将字符串转换成字节切片以后,两者之间毫无关联,因为 Go 会新分配一片内存空间给字节切片,再将字符串的内存复制过去,对字节切片进行修改不会对原字符串产生任何影响,这么做是为了内存安全。

在这种情况下,如果要转换的字符串或字节切片很大,那么性能开销就会很高。不过你也可以通过unsafe库来实现无复制转换,不过背后的安全问题需要自己承担,比如下面的例子,b1 和 s1 的地址是一样的。

go
func main() {
  s1 := "hello world"
  b1 := unsafe.Slice(unsafe.StringData(s1), len(s1))
  fmt.Printf("%p %p", unsafe.StringData(s1), unsafe.SliceData(b1))
}
0xe27bb2 0xe27bb2

长度

字符串的长度,其实并不是字符的个数,而是字节序列的长度。只是大多数时候我们处理的是 ASCII 字符,每个字符恰好能用一个字节表示,所以字节长度与字符个数恰好相等。求字符串长度使用内置函数len,例子如下:

go
func main() {
   str := "this is a string" // 看起来长度是16
   str2 := "这是一个字符串" // 看起来长度是7
   fmt.Println(len(str), len(str2))
}
16 21

看起来中文字符串比英文字符串短,但是实际求得的长度却比英文字符串长。这是因为在unicode编码中,一个汉字在大多数情况下占 3 个字节,一个英文字符只占一个字节,通过输出字符串第一个元素可以看出结果:

go
func main() {
   str := "this is a string"
   str2 := "这是一个字符串"
   fmt.Println(string(str[0]))
   fmt.Println(string(str2[0]))
   fmt.Println(string(str2[0:3]))
}
t // 字母t
è // 一个中文字符的“碎片”(第一个字节)的编码值,碰巧与意大利语字符 è 的编码值相同
这 // 中文汉字

拷贝

类似数组切片的拷贝方式,字符串拷贝其实是字节切片拷贝,使用内置函数copy

go
func main() {
   var dst, src string
   src = "this is a string"
   desBytes := make([]byte, len(src))
   copy(desBytes, src)
   dst = string(desBytes)
   fmt.Println(src, dst)
}

也可以使用strings.clone函数,但其实内部实现都差不多

go
func main() {
   var dst, src string
   src = "this is a string"
   dst = strings.Clone(src)
   fmt.Println(src, dst)
}

拼接

字符串的拼接使用+操作符

go
func main() {
   str := "this is a string"
   str = str + " that is a int"
   fmt.Println(str)
}

也可以转换为字节切片再进行添加元素

go
func main() {
   str := "this is a string"
   bytes := []byte(str)
   bytes = append(bytes, "that is a int"...)
   str = string(bytes)
   fmt.Println(str)
}

以上两种拼接方式性能都很差,一般情况下可以使用,但如果对应性能有更高要求,可以使用strings.Builder

go
func main() {
   builder := strings.Builder{}
   builder.WriteString("this is a string ")
   builder.WriteString("that is a int")
   fmt.Println(builder.String())
}
this is a string that is a int

遍历

在本文开头就已经提到过,Go 中的字符串就是一个只读的字节切片,也就是说字符串的组成单位是字节而不是字符。这种情况经常会在遍历字符串时遇到,例如下方的代码

go
func main() {
  str := "hello world!"
  for i := 0; i < len(str); i++ {
    fmt.Printf("%d,%x,%s\n", str[i], str[i], string(str[i]))
  }
}

例子中分别输出了字节的十进制形式和十六进制形式。

104,68,h
101,65,e
108,6c,l
108,6c,l
111,6f,o
32,20,
119,77,w
111,6f,o
114,72,r
108,6c,l
100,64,d
33,21,!

由于例子中的字符都是属于 ASCII 字符,只需要一个字节就能表示,所以结果恰巧每一个字节对应一个字符。但如果包含非 ASCII 字符结果就不同了,如下

go
func main() {
  str := "hello 世界!"
  for i := 0; i < len(str); i++ {
    fmt.Printf("%d,%x,%s\n", str[i], str[i], string(str[i]))
  }
}

通常情况下,一个中文字符会占用 3 个字节,所以就可能会看到以下结果

104,68,h
101,65,e
108,6c,l
108,6c,l
111,6f,o
32,20,
228,e4,ä
184,b8,¸
150,96,–
231,e7,ç
149,95,•
140,8c,Œ
33,21,!

按照字节来遍历会把中文字符拆开,这显然会出现乱码。Go 字符串是明确支持 UTF-8 的,应对这种情况就需要用到rune类型,在使用for range进行遍历时,其默认的遍历单位类型就是一个rune,例如下方代码

go
func main() {
   str := "hello 世界!"
   for _, r := range str {
      fmt.Printf("%d,%x,%s\n", r, r, string(r))
   }
}

输出如下

104,68,h
101,65,e
108,6c,l
108,6c,l
111,6f,o
32,20,
19990,4e16,世
30028,754c,界
33,21,!

rune本质上是int32的类型别名,unicode 字符集的范围位于 0x0000 - 0x10FFFF 之间,最大也只有三个字节,合法的 UTF-8 编码最大字节数只有 4 个字节,所以使用int32来存储是理所当然,上述例子中将字符串转换成[]rune再遍历也是一样的道理,如下

go
func main() {
   str := "hello 世界!"
   runes := []rune(str)
   for i := 0; i < len(runes); i++ {
      fmt.Println(string(runes[i]))
   }
}

还可以使用utf8包下的工具,例如

go
func main() {
  str := "hello 世界!"
  for i, w := 0, 0; i < len(str); i += w {
    r, width := utf8.DecodeRuneInString(str[i:])
    fmt.Println(string(r))
    w = width
  }
}

这两个例子的输出都是相同的。

TIP

关于字符串的更多细节,可以前往Strings, bytes, runes and characters in Go了解。

Golang学习网由www.golangdev.cn整理维护