分享

Go 学习笔记(31)— 字符串 string、字符 rune、字节 byte、UTF

 新用户79878317 2021-07-24

1. 字符串 string 类型

Go 语言中字符串的内部实现使用 UTF-8 编码,通过 rune 类型,可以方便地对每个 UTF-8 字符进行访问。当然, Go 语言也支持按照传统的 ASCII 码方式逐字符进行访问。

  1. 字符串是常量,可以通过类似数组索引访问其字节单元,但是不能修改某个字节的值;
var a string = 'hello,world' b := a[0] a[1] = 'a' // error
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

内置的 len 函数可以返回一个字符串中的字节数目(不是 rune 字符数目)。

s := 'hello, world'
fmt.Println(len(s))     // '12'
fmt.Println(s[0], s[7]) // '104 119' ('h' and 'w')
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

注意:第 i 个字节并不一定是字符串的第 i 个字符,因为对于非 ASCII 字符的 UTF8 编码会要两个或多个字节。

  1. 字符串转换为切片 []byte(s) 要慎用,每转换一次需要复制一份内存,尤其是字符串数据量较大时;

  2. 字符串末尾不包含 NULL 字符,与 C/C++ 不一样;

  3. 字符串类型底层实现是一个二元的数据结构,一个是指针指向字节数组的起点,另一个是字符串的长度;

// runtime/string.go type stringStruct struct { str unsafe . Pointer //指向底层字节数组的指针 len int //字节数组长度 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  1. 基于字符串创建的切片和原字符串指向相同的底层字符数组,一样不能修改,对字符串的切片操作返回的子串仍是 string ,而非 slice

  2. 字符串和切片的转换:字符串可以转换为字节数组,也可以转换为 Unicode 数组;

a := 'hello , 世界!'
b := []byte(a)
c :=[]rune(a)
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3
  1. 字符串支持连接运算,可以通过 len() 获取字符串的长度; lenGolang 内置的一个函数,这个函数返回字符串的长度,切片长度,数组长度,通道长度。如果用于获取字符串长度时,当字符串为空, len 返回值是 0。即判断字符串是否为空,有以下两种方式:
var str string//string 类型变量在定义后默认的初始值是空,不是 nil。 if str == '' { // str 为空 }
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

或者

var str string
if len(str) == 0 {
    // str 为空
}
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

对于简单而少量的拼接,使用运算符+和+=的效果虽然很好,但随着拼接操作次数的增加,这种做法的效率并不高。如果需要在循环中拼接字符串,则使用空的字节缓冲区来拼接的效率更高。

func main() { var buffer bytes.Buffer for i := 0; i < 500; i++ { buffer.WriteString('hello,world') } fmt.Println(buffer.String()) // 对缓冲区调用函数String() 以字符串的方式输出结果。 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  1. 获取字符串中某个字节的地址属于非法行为,例如 &str[i]

  2. 使用双引号''表示字符串时不能跨行,如果想要在源码中嵌入一个多行字符串时,就必须使用 ` 反引号(Esc 下面的字符),代码如下:

package main

import 'fmt'

func main() {
str := `
first line,
second line,
third line,
\r
\n
`
// 在这种方式下,反引号间换行将被作为字符串中的换行,但是所有的转义字符均无效,文本将会原样输出。
fmt.Println(str)
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

输出:

first line, second line, third line, \r \n
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

示例代码如下:

package main

import 'fmt'

func main() {
var s string = 'hello'
var a string = ',world'
s1 := s[0]
s2 := s[:3]
s3 := []byte(s)
s4 := []rune(s)
sLength := len(s)
c := s + a
fmt.Printf('s1 is %v, s1 type is %T\n', s1, s1)
fmt.Printf('s2 is %v, s2 type is %T\n', s2, s2)
fmt.Printf('s3 is %v, s3 type is %T\n', s3, s3)
fmt.Printf('s4 is %v, s4 type is %T\n', s4, s4)
fmt.Printf('sLength is %v \n', sLength)
fmt.Printf('c is %v \n', c)
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

输出结果:

s1 is 104, s1 type is uint8 s2 is hel, s2 type is string s3 is [104 101 108 108 111], s3 type is []uint8 s4 is [104 101 108 108 111], s4 type is []int32 sLength is 5 c is hello,world
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

字符串值也可以用字符串面值方式编写,只要将一系列字节序列包含在双引号内即可:

'hello, 世界'
  • 1
  • 1

因为 Go 语言源文件总是用 UTF8 编码,并且 Go 语言的文本字符串也以 UTF8 编码的方式处理,因此我们可以将 Unicode 码点也写到字符串面值中。

在一个双引号包含的字符串面值中,可以用以反斜杠\开头的转义序列插入任意的数据。下面的换行、回车和制表符等是常见的 ASCII 控制代码的转义方式:

\a 响铃 \b 退格 \f 换页 \n 换行 \r 回车 \t 制表符 \v 垂直制表符 \' 单引号(只用在 '\'' 形式的rune符号面值中) \' 双引号(只用在 '...' 形式的字符串面值中) \\ 反斜杠
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

可以通过十六进制或八进制转义在字符串面值中包含任意的字节。

  • 一个十六进制的转义形式是\xhh,其中两个 h 表示十六进制数字(大写或小写都可以);
  • 一个八进制转义形式是\ooo,包含三个八进制的 o 数字(0 到 7),但是不能超过\377(译注:对应一个字节的范围,十进制为255);

len 用来获取字符串、切片、数组、通道、字典类型变量的内容长度,不同的数据类型,长度计算规则不一样。

  • 对于切片、字典、数组、通道类型的变量,它们中每一个元素就是一个长度;
  • 对于 string 类型变量,它们每一个字节是一个长度;
  • 对于 rune 类型切片变量,它们每一个字符是一个长度,rune 类型变量中的内容采用 UTF-8 编码,一个字符可能对应 4 个字节;

2. 字符 rune 类型

Go 默认的字符编码就是 UTF-8 类型。Go 语言的字符有以下两种:

  • 一种是 uint8 类型,或者叫 byte 型( byteunit8 的别名),代表了 ASCII 码的一个字符,占用 1 个字节。

  • 一种是 rune 类型,代表一个 UTF-8 字符,当需要处理中文、日文或者其他复合字符时,则需要用到 rune 类型。 rune 类型等价于 int32 类型,占用 4 个字节。

rune 类型是 int32 的一个别名, rune 主要用于表示 UTF-8 编码时的字符类型。

通常情况下一个字符就是一个字节,在 Golang 中用 byte 关键字来表示字节,而 UTF-8 编码的字符,可能会存在一个字符用三个字节表示。如果使用 byte 类型来存储 UTF-8 编码的字符串,就会导致读取单个字节时值没有意义的情况。

所以 Golang 中使用 rune 来存储 UTF-8 编码的字符。

package main

import 'fmt'

func main() {
var str string = '中国'
rangeRune([]rune(str))
rangeStr(str)
}

func rangeRune(arg []rune) {
fmt.Println('rune type arg length is ', len(arg))
for i := 0; i < len(arg); i++ {
fmt.Printf('i is %d, value is %c\n', i, arg[i])
}
}

func rangeStr(arg string) {
fmt.Println('str type arg length is ', len(arg))
for i := 0; i < len(arg); i++ {
fmt.Printf('i is %d, value is %c\n', i, arg[i])
}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

输出结果:

rune type arg length is 2 i is 0, value is 中 i is 1, value is 国 str type arg length is 6 i is 0, value is ä i is 1, value is ¸ i is 2, value is ­ i is 3, value is å i is 4, value is › i is 5, value is ½
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

可以看到,当使用 byte 来读取字符时,出现了乱码现象,主要原因是一个汉字不是一个字节。所以当字符串中有汉字时,采用 rune 类型能够比较方便地存储和读取。

rune 类型变量默认初始值是 0。

3. 字节 byte 类型

byte 用来表示字节,一个字节是 8 位。定义一个字节类型变量的语法是:

var b1 byte
var b2 = 'c'
var b3 byte = 'c'
b4 := 'c'
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

byte 类型变量默认初始值是 0。byte 类型是 uint8 类型的一个别名。

func main() { s := 'hello 世界' runeSlice := []rune(s) // len = 8 byteSlice := []byte(s) // len = 12 // 打印每个rune切片元素 for i:= 0; i < len(runeSlice); i++ { fmt.Println(runeSlice[i]) // 输出104 101 108 108 111 32 19990 30028 } fmt.Println() // 打印每个byte切片元素 for i:= 0; i < len(byteSlice); i++ { fmt.Println(byteSlice[i]) // 输出104 101 108 108 111 32 228 184 150 231 149 140 } }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

我们可以看到,因为Go中的字符串采用UTF-8编码,且由于rune类型是4个字节,所以切片[]rune中,一个rune切片中的单个元素(4个字节),就能够完整的容纳一个UTF-8编码的中文字符(3个字节);而在[]byte中,由于每个byte切片元素只有1个字节,所以需要3个byte切片元素来表示一个中文字符。这样,用[]byte表示的字符串就要比[]rune表示的字符串,切片长度多4(6 - 2),打印结果符合预期。
所以,我个人认为设计rune类型的目的,就是为了更方便的表示类似中文的非英文字符,处理起来更加方便;而byte类型则对英文字符的处理更加友好。

4. UTF-8 和 Unicode 有何区别

UnicodeASCII 类似,都是一种字符集。

Unicode 收集了这个世界上所有的符号系统,包括重音符号和其它变音符号,制表符和回车符,还有很多神秘的符号,每个符号都分配一个唯一的 Unicode 码点, Unicode 码点对应 Go 语言中的 rune 整数类型( runeint32 等价类型)。

我们可以将一个符号序列表示为一个 int32 序列。这种编码方式叫 UTF-32UCS-4 ,每个 Unicode 码点都使用同样大小的 32bit 来表示。这种方式比较简单统一,但是它会浪费很多存储空间,因为大多数计算机可读的文本是 ASCII 字符,本来每个 ASCII 字符只需要 8bit 或 1 字节就能表示。而且即使是常用的字符也远少于 65,536 个,也就是说用 16bit 编码方式就能表达常用字符。但是,还有其它更好的编码方法吗?

UTF8 是一个将 Unicode 码点编码为字节序列的变长编码。 UTF8 编码使用 1 到 4 个字节来表示每个 Unicode 码点, ASCII 部分字符只使用 1 个字节,常用字符部分使用 2 或 3 个字节表示。

UTF-16编码存在一定的问题:无论是ASCII中定义的英文字符,还是复杂的中文字符,它都采用2个字节来存储。如果严格按照2个字节存储,编码号比较小的(如英文字母)的许多高位都为0(如字母t:00000000 01110100)。

UTF-16、UTF-8、还有其他五花八门的编码存储方式,都是Unicode的底层存储实现。用编程范式的语言来描述:Unicode是接口,定义了有哪些映射规则;而UTF-8、UTF-16则是Unicode这个接口的实现,它们在计算机底层实现了这些映射规则。

每个符号编码后第一个字节的高端 bit 位用于表示编码总共有多少个字节。如果第一个字节的高端 bit为 0,则表示对应 7bit 的 ASCII 字符, ASCII 字符每个字符依然是一个字节,和传统的 ASCII 编码兼容。如果第一个字节的高端 bit 是 110,则说明需要 2 个字节;后续的每个高端 bit 都以 10 开头。更大的 Unicode 码点也是采用类似的策略处理。

0xxxxxxx                             runes 0-127    (ASCII)
110xxxxx 10xxxxxx                    128-2047       (values <128 unused)
1110xxxx 10xxxxxx 10xxxxxx           2048-65535     (values <2048 unused)
11110xxx 10xxxxxx 10xxxxxx 10xxxxxx  65536-0x10ffff (other values unused)
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

变长的编码无法直接通过索引来访问第 n 个字符,但是 UTF8 编码获得了很多额外的优点:

  1. 首先 UTF8 编码比较紧凑,完全兼容 ASCII 码,并且可以自动同步;
  2. 它可以通过向前回朔最多 3 个字节就能确定当前字符编码的开始字节的位置。它也是一个前缀编码,所以当从左向右解码时不会有任何歧义也并不需要向前查看(译注:像 GBK 之类的编码,如果不知道起点位置则可能会出现歧义)。
  3. 没有任何字符的编码是其它字符编码的子串,或是其它编码序列的字串,因此搜索一个字符时只要搜索它的字节编码序列即可,不用担心前后的上下文会对搜索结果产生干扰。同时 UTF8 编码的顺序和 Unicode 码点的顺序一致,因此可以直接排序 UTF8 编码序列。同时因为没有嵌入的 NULL (0)字节,可以很好地兼容那些使用 NULL 作为字符串结尾的编程语言。

Go 语言的源文件采用 UTF8 编码,并且 Go 语言处理 UTF8 编码的文本也很出色。 unicode 包提供了诸多处理 rune 字符相关功能的函数(比如区分字母和数字,或者是字母的大写和小写转换等), unicode/utf8 包则提供了用于 rune 字符序列的 UTF8 编码和解码的功能。

Go 语言字符串面值中的 Unicode 转义字符让我们可以通过 Unicode 码点输入特殊的字符。有两种形式:

  1. \uhhhh对应 16bit 的码点值;
  2. \Uhhhhhhhh对应 32bit 的码点值;

其中 h 是一个十六进制数字,一般很少需要使用 32bit 的形式。每一个对应码点的 UTF8 编码。例如:下面的字母串面值都表示相同的值:

'世界' '\xe4\xb8\x96\xe7\x95\x8c' '\u4e16\u754c' '\U00004e16\U0000754c'
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

上面三个转义序列都为第一个字符串提供替代写法,但是它们的值都是相同的。

Unicode 转义也可以使用在 rune 字符中。下面三个字符是等价的:

'世' '\u4e16' '\U00004e16'
  • 1
  • 1

对于小于 256 的码点值可以写在一个十六进制转义字节中,例如\x41对应字符 'A' ,但是对于更大的码点则必须使用\u\U转义形式。因此,\xe4\xb8\x96并不是一个合法的 rune 字符,虽然这三个字节对应一个有效的 UTF8 编码的码点。

我们看下包含了中西两种字符。如下所示字符串包含 13 个字节,以 UTF8 形式编码,但是只对应 9 个 Unicode 字符:

import 'unicode/utf8' s := 'Hello, 世界' fmt.Println(len(s)) // '13' fmt.Println(utf8.RuneCountInString(s)) // '9'
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

为了处理这些真实的字符,我们需要一个 UTF8 解码器。 unicode/utf8 包提供了该功能,我们可以这样使用:

for i := 0; i < len(s); {
    r, size := utf8.DecodeRuneInString(s[i:])
    fmt.Printf('%d\t%c\n', i, r)
    i += size
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

字符集为每个字符分配一个唯一的 ID,我们使用到的所有字符在 Unicode 字符集中都有一个唯一的 ID,例如上面例子中的 a 在 Unicode 与 ASCII 中的编码都是 97。汉字“你”在 Unicode 中的编码为 20320,在不同国家的字符集中,字符所对应的 ID 也会不同。而无论任何情况下,Unicode 中的字符的 ID 都是不会变化的。

UTF-8 是编码规则,将 Unicode 中字符的 ID 以某种方式进行编码,UTF-8 的是一种变长编码规则,从 1 到 4 个字节不等。编码规则如下:

  • 0xxxxxx 表示文字符号 0~127,兼容 ASCII 字符集。
  • 从 128 到 0x10ffff 表示其他字符。

根据这个规则,拉丁文语系的字符编码一般情况下每个字符占用一个字节,而中文每个字符占用 3 个字节。

广义的 Unicode 指的是一个标准,它定义了字符集及编码规则,即 Unicode 字符集和 UTF-8、UTF-16 编码等。

5. golang 中获取字符串长度

5.1 不同编码符串定义

因为不同字符具有不同的编码格式。拉丁字母一个字符只要一个字节就行,而中文则可能需要两到三个字节;UNICODE 把所有字符设置为 2 个字节,UTF-8 格式则把所有字符设置为 1–3 个字节。

因此,字符串长度的获得,不等于按字节数查找,而要根据不同字符编码查找。

5.2 获取字符串长度的方法

golang 有自己的默认判断长度函数 len() ;但遗憾的是,len() 函数判断字符串长度的时候,是判断字符的字节数而不是字符长度。因此,在中文字符下,应该采用如下方法:

  1. 使用 bytes.Count() 统计
  2. 使用 strings.Count() 统计
  3. 将字符串转换为 []rune 后调用 len 函数进行统计
  4. 使用 utf8.RuneCountInString()统计

由于中文字符可能占用 1-3 个字节,所以 len() 获取的长度会比其它的大一些。

package main import ( 'bytes' 'fmt' 'strings' 'unicode/utf8' ) func main() { s := 'hello,您好' s_length := len(s) fmt.Println(s_length) // 12 fmt.Println(len([]byte(s))) // 12 byte_length := f1(s) fmt.Println(byte_length) // 8 string_length := f2(s) fmt.Println(string_length) // 8 rune_length := f3(s) fmt.Println(rune_length) // 8 utf_length := f4(s) fmt.Println(utf_length) // 8 } func f1(s string) int { return bytes.Count([]byte(s), nil) - 1 } func f2(s string) int { return strings.Count(s, '') - 1 } func f3(s string) int { return len([]rune(s)) } func f4(s string) int { return utf8.RuneCountInString(s) }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44

6. 字符串和Byte切片

Go 语言中,一个 string 类型的值既可以被拆分为一个包含多个字符的序列,也可以被拆分为一个包含多个字节的序列。前者可以由一个以 rune 为元素类型的切片来表示,而后者则可以由一个以 byte 为元素类型的切片代表。

runeGo 语言特有的一个基本数据类型,它的一个值就代表一个字符,即:一个 Unicode 字符。比如,'G’、'o’、'爱’、'好’、'者’代表的就都是一个 Unicode 字符。我们已经知道,UTF-8 编码方案会把一个 Unicode 字符编码为一个长度在[1, 4]范围内的字节序列。所以,一个 rune 类型的值也可以由一个或多个字节来代表。

package main

import (
'fmt'
)

func main() {
str := 'Go爱好者'
fmt.Printf('The string: %q\n', str)
// %q 表示 该值对应的双引号括起来的 go 语法字符串字面值
// 一个rune类型的值在底层其实就是一个 UTF-8 编码值
fmt.Printf('  => runes(char): %q\n', []rune(str))
// %x 表示按照 16 进制数来显示
fmt.Printf('  => runes(hex): %x\n', []rune(str))
// 把每个字符的 UTF-8 编码值都拆成相应的字节序列
fmt.Printf('  => bytes(hex): [% x]\n', []byte(str))
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

输出结果:

The string: 'Go爱好者' => runes(char): ['G' 'o' '爱' '好' '者'] => runes(hex): [47 6f 7231 597d 8005] => bytes(hex): [47 6f e7 88 b1 e5 a5 bd e8 80 85]
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

注意,对于一个多字节的 UTF-8 编码值来说,我们可以把它当做一个整体转换为单一的整数,也可以先把它拆成字节序列,再把每个字节分别转换为一个整数,从而得到多个整数。

这两种表示法展现出来的内容往往会很不一样。比如,对于中文字符’爱’来说,它的 UTF-8 编码值可以展现为单一的整数7231,也可以展现为三个整数,即:e7、88和b1。

总之,一个 string 类型的值会由若干个 Unicode 字符组成,每个 Unicode 字符都可以由一个rune 类型的值来承载。这些字符在底层都会被转换为 UTF-8 编码值,而这些 UTF-8 编码值又会以字节序列的形式表达和存储。

因此,一个 string 类型的值在底层就是一个能够表达若干个 UTF-8 编码值的字节序列。

package main

import 'fmt'

func main() {
str := 'Go爱好者'
for i, c := range str {
fmt.Printf('%d: %q [% x]\n', i, c, []byte(string(c)))
}
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

输出结果:

0: 'G' [47] 1: 'o' [6f] 2: '爱' [e7 88 b1] 5: '好' [e5 a5 bd] 8: '者' [e8 80 85]
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

由此可以看出,这样的 for 语句可以逐一地迭代出字符串值里的每个 Unicode 字符。但是,相邻的 Unicode 字符的索引值并不一定是连续的。这取决于前一个 Unicode 字符是否为单字节字符。正因为如此,如果我们想得到其中某个 Unicode 字符对应的 UTF-8 编码值的宽度,就可以用下一个字符的索引值减去当前字符的索引值。

6.1 字符串和Byte 切换

首先:一个值在从 string 类型向 []byte 类型转换时代表着以 UTF-8 编码的字符串会被拆分成零散、独立的字节。

除了与 ASCII 编码兼容的那部分字符集,以 UTF-8 编码的某个单一字节是无法代表一个字符的。

string([]byte{'\xe4', '\xbd', '\xa0', '\xe5', '\xa5', '\xbd'}) // 你好
  • 1
  • 1

比如,UTF-8 编码的三个字节\xe4、\xbd和\xa0合在一起才能代表字符’你’,而\xe5、\xa5和\xbd合在一起才能代表字符’好’。

其次:一个值在从 string 类型向 []rune 类型转换时代表着字符串会被拆分成一个个 Unicode 字符。

string([]rune{'\u4F60', '\u597D'}) // 你好
  • 1
  • 1

完整示例代码:

package main

import (
'fmt'
)

func main() {
srcStr := '你好'
fmt.Printf('The string: %q\n', srcStr)            // The string: '你好'
fmt.Printf('The hex of %q: %x\n', srcStr, srcStr) // The hex of '你好': e4bda0e5a5bd
fmt.Printf('The byte slice of %q: % x\n', srcStr, []byte(srcStr))
//The byte slice of '你好': e4 bd a0 e5 a5 bd
fmt.Printf('The string: %q\n', string([]byte{'\xe4', '\xbd', '\xa0', '\xe5', '\xa5', '\xbd'}))
// The string: '你好'
fmt.Printf('The rune slice of %q: %U\n', srcStr, []rune(srcStr))
// The rune slice of '你好': [U+4F60 U+597D]
fmt.Printf('The string: %q\n', string([]rune{'\u4F60', '\u597D'}))
// The string: '你好'
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

6.2 字符串处理标准包

标准库中有四个包对字符串处理尤为重要: bytesstringsstrconvunicode 包。

  • strings 包提供了许多如字符串的查询、替换、比较、截断、拆分和合并等功能。
  • bytes 包也提供了很多类似功能的函数,但是针对和字符串有着相同结构的 []byte 类型。因为字符串是只读的,因此逐步构建字符串会导致很多分配和复制。在这种情况下,使用 bytes.Buffer 类型将会更有效。
  • strconv 包提供了布尔型、整型数、浮点数和对应字符串的相互转换,还提供了双引号转义相关的转换。
  • unicode 包提供了 IsDigitIsLetterIsUpperIsLower 等类似功能,它们用于给字符分类。每个函数有一个单一的 rune 类型的参数,然后返回一个布尔值。而像 ToUpperToLower 之类的转换函数将用于 rune 字符的大小写转换。所有的这些函数都是遵循 Unicode 标准定义的字母、数字等分类规范。 strings 包也有类似的函数,它们是 ToUpperToLower ,将原始字符串的每个字符都做相应的转换,然后返回新的字符串。

下面例子的 basename 函数灵感源于 Unix shell 的同名工具。在我们实现的版本中, basename(s) 将看起来像是系统路径的前缀删除,同时将看似文件类型的后缀名部分删除:

fmt.Println(basename('a/b/c.go')) // 'c' fmt.Println(basename('c.d.go')) // 'c.d' fmt.Println(basename('abc')) // 'abc'
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

pathpath/filepath 包提供了关于文件路径名更一般的函数操作。使用斜杠分隔路径可以在任何操作系统上工作。斜杠本身不应该用于文件名,但是在其他一些领域可能会用于文件名,例如 URL 路径组件。

相比之下, path/filepath 包则使用操作系统本身的路径规则,例如 POSIX 系统使用 /foo/bar ,而 Microsoft Windows 使用c:\foo\bar等。

一个字符串是包含只读字节的数组,一旦创建,是不可变的。相比之下,一个字节 slice 的元素则可以自由地修改。
字符串和字节 slice 之间可以相互转换:

s := 'abc'
b := []byte(s)
s2 := string(b)
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

从概念上讲,一个 []byte(s) 转换是分配了一个新的字节数组用于保存字符串数据的拷贝,然后引用这个底层的字节数组。编译器的优化可以避免在一些场景下分配和复制字符串数据,但总的来说需要确保在变量 b 被修改的情况下,原始的 s 字符串也不会改变。将一个字节 slice 转换到字符串的 string(b) 操作则是构造一个字符串拷贝,以确保 s2 字符串是只读的。

为了避免转换中不必要的内存分配, bytes 包和 strings 同时提供了许多实用函数。下面是 strings 包中的六个函数:

func Contains(s, substr string) bool func Count(s, sep string) int func Fields(s string) []string func HasPrefix(s, prefix string) bool func Index(s, sep string) int func Join(a []string, sep string) string
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

bytes 包中也对应的六个函数:

func Contains(b, subslice []byte) bool
func Count(s, sep []byte) int
func Fields(s []byte) [][]byte
func HasPrefix(s, prefix []byte) bool
func Index(s, sep []byte) int
func Join(s [][]byte, sep []byte) []byte
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

它们之间唯一的区别是字符串类型参数被替换成了字节 slice 类型的参数。

bytes 包还提供了 Buffer 类型用于字节 slice 的缓存。一个 Buffer 开始是空的,但是随着 stringbyte[]byte 等类型数据的写入可以动态增长,一个 bytes.Buffer 变量并不需要初始化,因为零值也是有效的:

// intsToString is like fmt.Sprint(values) but adds commas. func intsToString(values []int) string { var buf bytes.Buffer buf.WriteByte('[') for i, v := range values { if i > 0 { buf.WriteString(', ') } fmt.Fprintf(&buf, '%d', v) } buf.WriteByte(']') return buf.String() } func main() { fmt.Println(intsToString([]int{1, 2, 3})) // '[1, 2, 3]' }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

当向 bytes.Buffer 添加任意字符的 UTF8 编码时,最好使用 bytes.BufferWriteRune 方法,但是 WriteByte 方法对于写入类似 '['']'ASCII 字符则会更加有效。

7. 字符串和数字的转换

除了字符串、字符、字节之间的转换,字符串和数值之间的转换也比较常见。由 strconv 包提供这类转换功能。
将一个整数转为字符串有两种方法:

  • 一种方法是用 fmt.Sprintf 返回一个格式化的字符串;
  • 另一个方法是用 strconv.Itoa(“整数到ASCII”)
x := 123
y := fmt.Sprintf('%d', x)
fmt.Println(y, strconv.Itoa(x)) // '123 123'
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

FormatIntFormatUint 函数可以用不同的进制来格式化数字:

fmt.Println(strconv.FormatInt(int64(x), 2)) // '1111011'
  • 1
  • 1

fmt.Printf 函数的 %b%d%o%x 等参数提供功能往往比 strconv 包的 Format 函数方便很多,特别是在需要包含有附加额外信息的时候:

s := fmt.Sprintf('x=%b', x) // 'x=1111011'
  • 1
  • 1

如果要将一个字符串解析为整数,可以使用 strconv 包的 AtoiParseInt 函数,还有用于解析无符号整数的 ParseUint 函数:

x, err := strconv.Atoi('123') // x is an int y, err := strconv.ParseInt('123', 10, 64) // base 10, up to 64 bits
  • 1
  • 2
  • 1
  • 2

ParseInt 函数的第三个参数是用于指定整型数的大小;例如 16 表示 int16 ,0 则表示 int 。在任何情况下,返回的结果 y 总是 int64 类型,你可以通过强制类型转换将它转为更小的整数类型。

有时候也会使用 fmt.Scanf 来解析输入的字符串和数字,特别是当字符串和数字混合在一行的时候,它可以灵活处理不完整或不规则的输入。

参考:
Go 语言圣经

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多