Go 的字符串

224 阅读5分钟

「这是我参与2022首次更文挑战的第6天,活动详情查看:2022首次更文挑战」。

概念

在 Go 里面,字符串类型统称 string

字符串可以包含任意的数据,但是通常是用来包含可读的文本,字符串是 UTF-8 字符的一个序列(当字符为 ASCII 码表上的字符时则占用 1 个字节,其它字符根据需要占用 2-4 个字节)。\

UTF-8 是一种被广泛使用的编码格式,是文本文件的标准编码,其中包括 XML 和 JSON 在内也都使用该编码。由于该编码对占用字节长度的不定性,在Go语言中字符串也可能根据需要占用 1 至 4 个字节,这样做不仅减少了内存和硬盘空间占用,同时也不用像其它语言那样需要对使用 UTF-8 字符集的文本进行编码和解码。\

string 类型的数据是不可变的

  • 提高了字符串的并发安全性和存储利用率
  • Go 语言规定,字符串类型的值在它的生命周期内是不可改变的
  • 如果声明了一个字符串类型的变量,是无法通过这个变量改变它对应的字符串值的,但字符串类型变量仍然可以进行二次赋值
  • 字符串是字节的定长数组
var s string = "hello"
s[1] = "s"	// 错误:字符串的内容不可改变
s = "gopher"	// 二次赋值是可以的,变量指向新的字符串

假如运行上述代码的话

# command-line-arguments
./18_string.go:6:7: cannot assign to s[1] (strings are immutable)

提示 strings are immutable 字符串是不可变的

好处

  • 不用再担心字符串的并发安全问题,Go 字符串可以被多个 Goroutine(Go 语言的轻量级用户线程)共享,开发者不用因为担心并发安全问题,使用会带来一定开销的同步机制
  • 由于字符串的不可变性,针对同一个字符串值,无论它在程序的几个位置被使用,Go 编译器只需要为它分配一块存储就好了,大大提高了存储利用率

获取字符串长度的时间复杂度是常数时间

无论该字符串字符个数有多少,都可以快速得到字符串的长度值,消除了获取字符串长度的开销

“所见即所得”的原始字符串

通过反引号` `支持构造“所见即所得”的原始字符串,任意转义字符都不会起到转义的作用

package main

import "fmt"

func main() {
	var s string = `         ,_---~~~~~----._
    _,,_,*^____      _____*g*"*,--,
   / __/ /'     ^.  /      \ ^@q   f
  [  @f | @))    |  | @))   l  0 _/
   /   ~____ / __ _____/     \
    |           _l__l_           I
    }          [______]           I
    ]            | | |            |
    ]             ~ ~             |
    |                            |
     |                           |`

	fmt.Println(s)
}
  • 运行结果还是会得到一个和上面一样的图案
  • 这个字符串包含了诸多 ASCII 字符,还有转义符,但并不会有转义的作用

对非 ASCII 字符提供原生支持

  • 消除了源码在不同环境下显示乱码的可能
  • Go 语言源文件默认采用的是 Unicode 字符集,Unicode 字符集是目前市面上最流行的字符集,它囊括了几乎所有主流非 ASCII 字符(包括中文字符)
  • Go 字符串中的每个字符都是一个 Unicode 字符,并且这些 Unicode 字符是以 UTF-8 编码格式存储在内存当中的

Go 字符串的组成

Go 语言在看待 Go 字符串组成这个问题上,有两种视角

一种是字节视角

也就是和所有其它支持字符串的主流语言一样,Go 语言中的字符串值也是一个可空的字节序列,字节序列中的字节个数称为该字符串的长度,一个个的字节只是孤立数据,不表意

package main

import "fmt"

func main() {
	// 字节角度
	var s string = "中国人"
	fmt.Printf("the length is %d \n", len(s))

	for i := 0; i < len(s); i++ {
		fmt.Printf("0x%x ", s[i])
	}
}

运行结果

the length is 9 
0xe4 0xb8 0xad 0xe5 0x9b 0xbd 0xe4 0xba 0xba 
  • 由“中国人”构成的字符串的字节序列长度为 9,一个中文 3 个字节
  • 仅从某一个输出的字节来看,它是不能与字符串中的任一个字符对应起来的

另一种角度,字符串由一个可空的字符序列构成

package main

import (
	"fmt"
	"unicode/utf8"
)

func main() {
	var s string = "中国人"
	fmt.Println("the char count in s is: ", utf8.RuneCountInString(s))
	for _, v := range s {
		fmt.Printf("0x%x ", v)
	}
}

运行结果

the char count in s is:  3
0x4e2d 0x56fd 0x4eba 
  • Go 采用的是 Unicode 字符集,每个字符都是一个 Unicode 字符,这里输出的 0x4e2d、0x56fd 和 0x4eba 是某种 Unicode 字符的表示
  • 以 0x4e2d为例,它是汉字“中”在 Unicode 字符集表中的码点(Code Point)

什么是 Unicode 码点呢

  • Unicode 字符集中的每个字符,都被分配了统一且唯一的字符编号
  • 所谓 Unicode 码点,就是指将 Unicode 字符集中的所有字符“排成一队”,字符在这个“队伍”中的位次,就是它在 Unicode 字符集中的码点
  • 一个码点唯一对应一个字符
  • “码点”的概念和 rune 类型有很大关系

rune 类型和字符字面值

  • Go 使用 rune 这个类型来表示一个 Unicode 码点
  • rune 本质上是 int32 类型的别名类型,它与 int32 类型是完全等价的
  • 在 Go 源码中可以看到它的定义,就是 int32 的类型别名
// $GOROOT/src/builtin.go
type rune = int32
  • 由于一个 Unicode 码点唯一对应一个 Unicode 字符
  • 所以一个 rune 实例就是一个 Unicode 字符,一个 Go 字符串也可以被视为 rune 实例的集合
  • 可以通过字符字面值来初始化一个 rune 变量

字符字面值

最常见的是通过单引号阔气的字符字面值

'a'  // ASCII 字符
'中'  // Unicode字符集中的中文字符
'\n' // 换行字符
''' // 单引号字符

Unicode 专用的转义字符\u 或\U 作为前缀

表示一个 Unicode 字符

'\u4e2d'     // 字符:中
'\U00004e2d' // 字符:中
'\u0027'     // 单引号字符
  • \u 后面接两个十六进制数
  • 如果是用两个十六进制数无法表示的 Unicode 字符,我们可以使用 \U,\U 后面可以接四个十六进制数来表示一个 Unicode 字符
  • 而且,由于表示码点的 rune 本质上就是一个整型数,所以还可用整型值来直接作为字符字面值给 rune 变量赋值
'\x27'  // 使用十六进制表示的单引号字符
'\047'  // 使用八进制表示的单引号字符

字符串字面值

字符串是字符的集合,字符串有多个字符

"中国人"
"\u4e2d\u56fd\u4eba" // 中国人
"\U00004e2d\U000056fd\U00004eba" // 中国人
"\xe4\xb8\xad\xe5\x9b\xbd\xe4\xba\xba" // 十六进制表示的字符串字面值:中国人

对 Unicode 字符(rune)进行编解码

标准库提供了 UTF-8 包

package main

import (
	"fmt"
	"unicode/utf8"
)

func encodeRune() {
	var r rune = 0x4E2D
	fmt.Printf("the unicode charactor is %c\n", r) // 中

	buf := make([]byte, 3)
	// 对 rune 进行 utf-8 编码
	_ = utf8.EncodeRune(buf, r)
	fmt.Printf("utf-8 representation is 0x%X\n", buf)
}

func decodeRune() {
	var buf = []byte{0xE4, 0xB8, 0xAD}
	// 对 buf 进行解码
	r, _ := utf8.DecodeRune(buf)
	fmt.Printf("the unicode charactor after decoding [0xE4, 0xB8, 0xAD] is %s", string(r))
}

func main() {
	encodeRune()
	decodeRune()
}

运行结果

the unicode charactor is 中
utf-8 representation is 0xE4B8AD
the unicode charactor after decoding [0xE4, 0xB8, 0xAD] is