像极!bytes包和strings包的那些相似操作
对数据类型为字节切片 ([]byte
) 或字符串 (string
) 的对象的处理是我们在 Go 语言编程过程中最常见的操作。经过前面章节的学习后,我们知道字节切片本质上是一个 “三元组 (array, len, cap)”,而 字符串则是一个 “二元组 (str, len)”(如下图所示):
图 9-3-1:字节切片与字符串的运行时表示
Go 字节切片为内存中的字节序列提供了抽象,而 Go 字符串则代表了采用 UTF-8 编码的 Unicode 字符的数组。Go 标准库中的 bytes
包和 strings
包分别为字节切片和字符串这两种抽象类型提供了基本操作类 API。这里之所以将这两个标准库包放在一起说明,是因为这两个包提供的 API 接口十分相似,在后面的具体 API 使用举例时大家可以感受到这一点。
bytes
和 strings
包提供的 API 几乎涵盖了所有基本操作,大致可分为如下几类:
- 查找与替换
- 比较
- 拆分
- 拼接
- 修剪和变换
- 快速创建实现了
io.Reader
接口的实例
本节中,我们就来了解一下如何使用这两个包对字节切片和字符串实现上述几类基本操作。一旦熟练掌握了这些常见基本操作的使用方法,处理字节切片和字符串以及由它们组成的复合类型数据对象时就可以游刃有余了。
1. 查找与替换
针对一个字符串 / 字节切片,我们经常做的操作包括查找其中是否存在某一个字符串。如果存在,那么返回该字符串在原字符串 / 字节切片中第一次出现时的位置信息 (下标)。有些时候,我们还会用另外一个字符串 / 字节切片对其进行替换。
1) 定性查找
所谓 “定性查找” 就是指返回有 (true
) 和无 (false
) 的查找。bytes
包和 strings
提供了一组名字相同的定性查找 API,包括:Contains
系列、HasPrefix
和 HasSuffix
。我们看下面用法示例 (示例源码位于 go-bytes-and-strings/search_and_replace.go
中):
Contains
函数
fmt.Println(strings.Contains("Golang", "Go")) // true
fmt.Println(strings.Contains("Golang", "go")) // false
fmt.Println(strings.Contains("Golang", "l")) // true
fmt.Println(strings.Contains("Golang", "")) // true
fmt.Println(strings.Contains("", "")) // true
fmt.Println(bytes.Contains([]byte("Golang"), []byte("Go"))) // true
fmt.Println(bytes.Contains([]byte("Golang"), []byte("go"))) // false
fmt.Println(bytes.Contains([]byte("Golang"), []byte("l"))) // true
fmt.Println(bytes.Contains([]byte("Golang"), []byte(""))) // true
fmt.Println(bytes.Contains([]byte("Golang"), nil)) // true
fmt.Println(bytes.Contains([]byte("Golang"), []byte{})) // true
fmt.Println(bytes.Contains(nil, nil)) // true
Contains
函数返回第一个参数代表的字符串 / 字节切片中是否包含第二个参数代表的字符串 / 字节切片。值得注意的是:在这个函数的语义中,任意字符串都包含空串 (""
),任意字节切片也都包含空字节切片 ([]byte{}
) 以及 nil
切片。
ContainsAny
函数
fmt.Println(strings.ContainsAny("Golang", "java")) // true
fmt.Println(strings.ContainsAny("Golang", "python")) // true
fmt.Println(strings.ContainsAny("Golang", "c")) // false
fmt.Println(strings.ContainsAny("Golang", "")) // false
fmt.Println(strings.ContainsAny("", "")) // false
fmt.Println(bytes.ContainsAny([]byte("Golang"), "java")) // true
fmt.Println(bytes.ContainsAny([]byte("Golang"), "c")) // false
fmt.Println(bytes.ContainsAny([]byte("Golang"), "")) // false
fmt.Println(bytes.ContainsAny(nil, "")) // false
ContainsAny
函数的语义是把其两个参数看成是两个 Unicode 字符的集合,如果两个集合存在不为空的交集,则返回 true
。以 strings.ContainsAny("Golang", "java")
为例,第一个参数对应的 Unicode 字符集合为 {‘G’, ‘o’, ‘l’, ‘a’, ‘n’, ‘g’},第二个参数对应的集合为 {‘j’, ‘a’, ‘v’},两个集合存在不为空的交集:{‘a’},因此该函数返回 true
。
ContainsRune
函数
fmt.Println(strings.ContainsRune("Golang", 97)) // true 字符[a]的Unicode码点 = 97
fmt.Println(strings.ContainsRune("Golang", rune('中'))) // false
fmt.Println(bytes.ContainsRune([]byte("Golang"), 97)) // true 字符[a]的Unicode码点 = 97
fmt.Println(bytes.ContainsRune([]byte("Golang"), rune('中'))) // false
ContainsRune
用于判断某一个 Unicode 字符 (以码点形式即 rune 类型值传入) 是否包含在第一个参数代表的字符串或字节切片中。
HasPrefix
和HasSuffix
函数
fmt.Println(strings.HasPrefix("Golang", "Go")) // true
fmt.Println(strings.HasPrefix("Golang", "Golang")) // true
fmt.Println(strings.HasPrefix("Golang", "lang")) // false
fmt.Println(strings.HasPrefix("Golang", "")) // true
fmt.Println(strings.HasPrefix("", "")) // true
fmt.Println(strings.HasSuffix("Golang", "Go")) // false
fmt.Println(strings.HasSuffix("Golang", "Golang")) // true
fmt.Println(strings.HasSuffix("Golang", "lang")) // true
fmt.Println(strings.HasSuffix("Golang", "")) // true
fmt.Println(strings.HasSuffix("", "")) // true
fmt.Println(bytes.HasPrefix([]byte("Golang"), []byte("Go"))) // true
fmt.Println(bytes.HasPrefix([]byte("Golang"), []byte("Golang"))) // true
fmt.Println(bytes.HasPrefix([]byte("Golang"), []byte("lang"))) // false
fmt.Println(bytes.HasPrefix([]byte("Golang"), []byte{})) // true
fmt.Println(bytes.HasPrefix([]byte("Golang"), nil)) // true
fmt.Println(bytes.HasPrefix(nil, nil)) // true
fmt.Println(bytes.HasSuffix([]byte("Golang"), []byte("Go"))) // false
fmt.Println(bytes.HasSuffix([]byte("Golang"), []byte("Golang"))) // true
fmt.Println(bytes.HasSuffix([]byte("Golang"), []byte("lang"))) // true
fmt.Println(bytes.HasSuffix([]byte("Golang"), []byte{})) // true
fmt.Println(bytes.HasSuffix([]byte("Golang"), nil)) // true
fmt.Println(bytes.HasSuffix(nil, nil)) // true
HasPrefix
函数用于判断第二个参数代表的字符串 / 字节切片是否是第一个参数的前缀,同理,HasSuffix
函数则用于判断第二个参数是否是第一个参数的后缀。要注意的是:在这两个函数的语义中,空字符串 (""
) 是任何字符串的前缀亦是后缀;空字节切片 ([]byte{}
) 和 nil 切片也是任何字节切片的前缀和后缀。
2) 定位查找
和定性查找相比,定位相关查找函数会给出第二个参数代表的字符串 / 字节切片在第一个参数中第一次出现的位置 (下标),如果没有找到,则返回 - 1。另外定位查找还有方向性,从左到右为正向定位查找 (Index
系列),反之为反向定位查找 (LastIndex
系列)。
// 定位查找(string)
fmt.Println(strings.Index("Learn Golang, Go!", "Go")) // 6
fmt.Println(strings.Index("Learn Golang, Go!", "")) // 0
fmt.Println(strings.Index("Learn Golang, Go!", "Java")) // -1
fmt.Println(strings.IndexAny("Learn Golang, Go!", "Java")) // 2
fmt.Println(strings.IndexRune("Learn Golang, Go!", rune('a'))) // 2
// 定位查找([]byte)
fmt.Println(bytes.Index([]byte("Learn Golang, Go!"), []byte("Go"))) // 6
fmt.Println(bytes.Index([]byte("Learn Golang, Go!"), nil)) // 0
fmt.Println(bytes.Index([]byte("Learn Golang, Go!"), []byte("Java"))) // -1
fmt.Println(bytes.IndexAny([]byte("Learn Golang, Go!"), "Java")) // 2
fmt.Println(bytes.IndexRune([]byte("Learn Golang, Go!"), rune('a'))) // 2
// 反向定位查找(string)
fmt.Println(strings.LastIndex("Learn Golang, Go!", "Go")) // 14
fmt.Println(strings.LastIndex("Learn Golang, Go!", "")) // 17
fmt.Println(strings.LastIndex("Learn Golang, Go!", "Java")) // -1
fmt.Println(strings.LastIndexAny("Learn Golang, Go!", "Java")) // 9
// 反向定位查找([]byte)
fmt.Println(bytes.LastIndex([]byte("Learn Golang, Go!"), []byte("Go"))) // 14
fmt.Println(bytes.LastIndex([]byte("Learn Golang, Go!"), nil)) // 17
fmt.Println(bytes.LastIndex([]byte("Learn Golang, Go!"), []byte("Java"))) // -1
fmt.Println(bytes.LastIndexAny([]byte("Learn Golang, Go!"), "Java")) // 9
和 ContainsAny
只查看交集是否为空不同,IndexAny
函数返回非空交集中第一个字符在第一个参数中的位置信息。另外要注意:反向查找空串或 nil 切片,返回的是第一个参数的长度,但作为位置 (下标) 信息,这个值已经是越界了的。
注:strings 包并未提供模糊查找功能,基于正则式的模糊查找可以使用标准库在
regexp
包中提供的实现。
3) 替换
Go 标准库在 strings
包中提供了两种进行字符串替换的方法:Replace
函数与 Replacer
类型。bytes
包中则只提供了 Replace
函数用于字节切片的替换:
// 替换(string)
fmt.Println(strings.Replace("I love java, java, java!!", "java", "go", 1)) // I love go, java, java!!
fmt.Println(strings.Replace("I love java, java, java!!", "java", "go", 2)) // I love go, go, java!!
fmt.Println(strings.Replace("I love java, java, java!!", "java", "go", -1)) // I love go, go, go!!
fmt.Println(strings.Replace("math", "", "go", -1)) // gomgoagotgohgo
fmt.Println(strings.ReplaceAll("I love java, java, java!!", "java", "go")) // I love go, go, go!!
replacer := strings.NewReplacer("java", "go", "python", "go")
fmt.Println(replacer.Replace("I love java, python, go!!")) // I love go, go, go!!
// 替换([]byte)
fmt.Printf("%s\n", bytes.Replace([]byte("I love java, java, java!!"), []byte("java"), []byte("go"), 1)) // I love go, java, java!!
fmt.Printf("%s\n", bytes.Replace([]byte("I love java, java, java!!"), []byte("java"), []byte("go"), 2)) // I love go, go, java!!
fmt.Printf("%s\n", bytes.Replace([]byte("I love java, java, java!!"), []byte("java"), []byte("go"), -1)) // I love go, go, go!!
fmt.Printf("%s\n", bytes.Replace([]byte("math"), nil, []byte("go"), -1)) // gomgoagotgohgo
fmt.Printf("%s\n", bytes.ReplaceAll([]byte("I love java, java, java!!"), []byte("java"), []byte("go"))) // I love go, go, go!!
通过上述示例我们看到:
Replace
函数广泛用于简单的字符串 / 字节切片替换。它的最后一个参数是一个整型数,用于控制进行替换的次数。如果传入-1
,则是全部替换。而另外一个ReplaceAll
函数本质上就等价于最后一个参数传入-1
的Replace
函数:
// $GOROOT/src/strings/strings.go
func Replace(s, old, new string, n int) string {
... ...
}
func ReplaceAll(s, old, new string) string {
return Replace(s, old, new, -1)
}
当参数
old
传入空字符串""
或nil
(仅字节切片) 时,Replace
会将new
参数所表示的要替入的字符串 / 字节切片 “插入” 原字符串 / 字节切片的每两个字符 (字节) 间的 “空隙” 中。当然原字符串 / 字节切片的首尾也会被各插入一个new
参数值。Replace
函数一次只能传入一组old
(替出) 和new
(替入) 字符串,而Replacer
类型实例化时则可以传入多组old
和new
参数,这样后续在使用Replacer.Replace
方法对原字符串进行替换时,可以一次实施多组不同字符串的替换。
2. 比较
1) 等值比较 (equality comparison
)
根据 Go 语言规范,切片类型变量之间不能直接通过操作符进行等值比较,但可以与 nil
做等值比较:
// go-bytes-and-strings/byte_slice_test_equality_with_operator.go
... ...
func main() {
var a = []byte{'a', 'b', 'c'}
var b = []byte{'a', 'b', 'd'}
if a == b { // 错误:invalid operation: a == b
fmt.Println("slice a is equal to slice b")
} else {
fmt.Println("slice a is not equal to slice b")
}
if a != nil { // 正确:valid operation
fmt.Println("slice a is not nil")
}
}
但 Go 语言原生支持通过操作符 ==
或 !=
对 string
类型变量进行等值比较,因此 strings
包未像 bytes
包那样提供了 Equal
函数。而 bytes
包的 Equal
函数的实现也是基于原生字符串类型的等值比较的:
// $GOROOT/src/bytes/bytes.go
func Equal(a, b []byte) bool {
return string(a) == string(b)
}
Go 编译器会为上面这个函数实现中的显式类型转换提供默认优化,不会额外为显式转型分配内存空间。明确了实现原理,下面例子输出的结果就很容易理解了:
// go-bytes-and-strings/byte_slice_equality.go
... ...
func main() {
fmt.Println(bytes.Equal([]byte{'a', 'b', 'c'}, []byte{'a', 'b', 'd'})) // false "abc" != "abd"
fmt.Println(bytes.Equal([]byte{'a', 'b', 'c'}, []byte{'a', 'b', 'c'})) // true "abc" == "abc"
fmt.Println(bytes.Equal([]byte{'a', 'b', 'c'}, []byte{'b', 'a', 'c'})) // false "abc" != "bac"
fmt.Println(bytes.Equal([]byte{}, nil)) // true "" == ""
}
strings
和 bytes
包还共同提供了 EqualFold
函数,用于进行大小写不敏感的 Unicode 字符的等值比较。字节切片在比较时,切片内的字节序列将被解释成字符的 UTF-8 编码表示后再行比较:
// go-bytes-and-strings/equalfold.go
... ...
func main() {
fmt.Println(strings.EqualFold("GoLang", "golang")) // true
fmt.Println(bytes.Equal([]byte("GoLang"), []byte("Golang"))) // false
fmt.Println(bytes.EqualFold([]byte("GoLang"), []byte("Golang"))) // true
}
2) 排序比较 (order comparison
)
bytes
和 strings
均提供了 Compare
方法用于对两个字符串 / 字节切片做排序比较。但 Go 原生支持通过操作符 >
、>=
、<
和 <=
对字符串类型变量进行排序比较,strings
包中 Compare
函数存在的意义更多是为了与 bytes
包尽量保持 API 的一致,其自身也是使用原生排序比较操作符实现的:
// $GOROOT/src/strings/compare.go
func Compare(a, b string) int {
if a == b {
return 0
}
if a < b {
return -1
}
return +1
}
实际应用中,我们很少使用 strings.Compare
,更多的是直接使用排序比较操作符对字符串类型变量进行比较。
bytes
包的 Compare
按字典序对两个字节切片中的内容进行比较,下面是其应用示例:
// go-bytes-and-strings/bytes_compare.go
... ...
func main() {
var a = []byte{'a', 'b', 'c'}
var b = []byte{'a', 'b', 'd'}
var c = []byte{} //empty slice
var d []byte //nil slice
fmt.Println(bytes.Compare(a, b)) // -1 a < b
fmt.Println(bytes.Compare(b, a)) // 1 b < a
fmt.Println(bytes.Compare(c, d)) // 0
fmt.Println(bytes.Compare(c, nil)) // 0
fmt.Println(bytes.Compare(d, nil)) // 0
fmt.Println(bytes.Compare(nil, nil)) // 0 nil == nil
}
3. 分割 (Split
)
我们在日常开发中经常遇到下面这样从类似 CSV (逗号分隔) 格式数据中提取分段数据的场景:
一条CVS数据:"tonybai,programmer,China"
提取出数据: ["tonybai", "programmer", "China"]
通过 Go 标准库的 strings
和 bytes
提供的对字符串 / 字节切片进行分割的 API,我们可以轻松应对这些问题。
1) Fields 相关函数
空白分割的字符串是我们能遇到的最简单的也是最常见的由特定分隔符分隔的数据,strings
包和 bytes
包中的 Fields
函数可直接用于处理这类数据的分割,我们看下面示例:
// go-bytes-and-strings/split_and_fields.go
fmt.Printf("%q\n", strings.Fields("go java python")) // ["go" "java" "python"]
fmt.Printf("%q\n", strings.Fields("\tgo \f \u0085 \u00a0 java \n\rpython")) // ["go" "java" "python"]
fmt.Printf("%q\n", strings.Fields(" \t \n\r ")) // []
fmt.Printf("%q\n", bytes.Fields([]byte("go java python"))) // ["go" "java" "python"]
fmt.Printf("%q\n", bytes.Fields([]byte("\tgo \f \u0085 \u00a0 java \n\rpython"))) // ["go" "java" "python"]
fmt.Printf("%q\n", bytes.Fields([]byte(" \t \n\r "))) // []
Fields
函数采用了 Unicode 空白字符的定义,下面字符均会被识别为空白字符:
// $GOROOT/src/unicode/graphic.go
'\t', '\n', '\v', '\f', '\r', ' ', U+0085 (NEL), U+00A0 (NBSP)
同时,通过示例我们看到 Fields
会忽略掉输入数据前后的空白字符以及中间连续的空白字符;并且如果输入数据仅包含空白字符,那么该函数将返回一个空的 string 类型切片。
Go 标准库还提供了灵活定制分割逻辑的 FieldsFunc
函数,通过传入一个用于指示是否为 “空白” 字符的函数,我们可以实现按自定义逻辑对原字符串进行分割:
// go-bytes-and-strings/split_and_fields.go
splitFunc := func(r rune) bool {
return r == rune('\n')
}
fmt.Printf("%q\n", strings.FieldsFunc("\tgo \f \u0085 \u00a0 java \n\n\rpython", splitFunc)) // ["\tgo \f \u0085 \u00a0 java " "\rpython"]
fmt.Printf("%q\n", bytes.FieldsFunc([]byte("\tgo \f \u0085 \u00a0 java \n\n\rpython"), splitFunc)) // ["\tgo \f \u0085 \u00a0 java " "\rpython"]
在上面这个例子里,我们通过传入的 splitFunc
指示 FieldsFunc
仅 \n
是空白字符,于是 FieldsFunc
仅将原字符串分割为两个字符串 (或字节切片)。
2) Split 相关函数
我们不仅可以使用空白对字符串进行分隔,理论上可使用任意字符作为字符串的分隔符。Go 标准库提供了 Split
相关函数可以更为通用地对字符串或字节切片进行分割:
// go-bytes-and-strings/split_and_fields.go
// 使用Split相关函数分割字符串
fmt.Printf("%q\n", strings.Split("a,b,c", ",")) // ["a" "b" "c"]
fmt.Printf("%q\n", strings.Split("a,b,c", "b")) // ["a," ",c"]
fmt.Printf("%q\n", strings.Split("Go社区欢迎你", "")) // ["G" "o" "社" "区" "欢" "迎" "你"]
fmt.Printf("%q\n", strings.Split("abc", "de")) // ["abc"]
fmt.Printf("%q\n", strings.SplitN("a,b,c,d", ",", 2)) // ["a" "b,c,d"]
fmt.Printf("%q\n", strings.SplitN("a,b,c,d", ",", 3)) // ["a" "b" "c,d"]
fmt.Printf("%q\n", strings.SplitAfter("a,b,c,d", ",")) // ["a," "b," "c," "d"]
fmt.Printf("%q\n", strings.SplitAfterN("a,b,c,d", ",", 2)) // ["a," "b,c,d"]
// 使用Split相关函数分割字节切片
fmt.Printf("%q\n", bytes.Split([]byte("a,b,c"), []byte(","))) // ["a" "b" "c"]
fmt.Printf("%q\n", bytes.Split([]byte("a,b,c"), []byte("b"))) // ["a," ",c"]
fmt.Printf("%q\n", bytes.Split([]byte("Go社区欢迎你"), nil)) // ["G" "o" "社" "区" "欢" "迎" "你"]
fmt.Printf("%q\n", bytes.Split([]byte("abc"), []byte("de"))) // ["abc"]
fmt.Printf("%q\n", bytes.SplitN([]byte("a,b,c,d"), []byte(","), 2)) // ["a" "b,c,d"]
fmt.Printf("%q\n", bytes.SplitN([]byte("a,b,c,d"), []byte(","), 3)) // ["a" "b" "c,d"]
fmt.Printf("%q\n", bytes.SplitAfter([]byte("a,b,c,d"), []byte(","))) // ["a," "b," "c," "d"]
fmt.Printf("%q\n", bytes.SplitAfterN([]byte("a,b,c,d"), []byte(","), 2)) // ["a," "b,c,d"]
通过上面示例,我们看到:
Split
函数既可以处理以逗号作为分隔符的字符串,亦可以处理以普通字母b
为分隔符的字符串;当传入空串 (或bytes.Split
被传入nil
切片) 作为分隔符时,Split
函数会按 UTF-8 的字符编码边界对 Unicode 进行分割,即每个 Unicode 字符都会被视为一个分割后的子字符串;如果原字符串中没有传入的分隔符,那么Split
会将原字符串作为返回的字符串切片中的唯一元素;SplitN
函数的最后一个参数表示对原字符串进行分割后产生的分段数量,Split
函数等价于给SplitN
的最后一个参数传入-1
;SplitAfter
与Split
的不同点在于它对原字符串 / 字节切片的分割点在每个分隔符的后面,由于分隔符并未真正起到 “分隔” 的作用,因此它不会被剔除掉,它也会作为子串的一部分返回;SplitAfterN
函数的最后一个参数表示对原字符串进行分割后产生的分段数量,SplitAfter
函数等价于给SplitAfterN
的最后一个参数传入-1
。
4. 拼接 (Concatenate
)
拼接是上面 “分割” 的逆过程。strings
和 bytes
包分别提供了各自的 Join
函数用于实现字符串或字节切片的拼接。
// go-bytes-and-strings/join_and_builder.go
s := []string{"I", "love", "Go"}
fmt.Println(strings.Join(s, " ")) // I love Go
b := [][]byte{[]byte("I"), []byte("love"), []byte("Go")}
fmt.Printf("%q\n", bytes.Join(b, []byte(" "))) // "I love Go"
strings
包还提供了 Builder
类型及相关方法用于高效地构建字符串,而 bytes
包与之对应的用于拼接切片的则是 Buffer
类型及相关方法:
// go-bytes-and-strings/join_and_builder.go
s := []string{"I", "love", "Go"}
var builder strings.Builder
for i, w := range s {
builder.WriteString(w)
if i != len(s)-1 {
builder.WriteString(" ")
}
}
fmt.Printf("%s\n", builder.String()) // I love Go
b := [][]byte{[]byte("I"), []byte("love"), []byte("Go")}
var buf bytes.Buffer
for i, w := range b {
buf.Write(w)
if i != len(b)-1 {
buf.WriteString(" ")
}
}
fmt.Printf("%s\n", buf.String()) // I love Go
注:在 “了解 string 实现原理与高效使用” 一节中我们曾经对多种字符串构造方式的性能做过横向比较,对字符串构建有性能要求的读者可以回顾一下那一节的内容。
5. 修剪与变换
1) 修剪
在对输入的数据进行处理之前,我们经常会对其做一些修剪,比如:把输入数据中首部和尾部多余的空白去除、去掉特定后缀信息等。Go 标准库 bytes
包和 strings
包提供了一系列 Trim
API 可以辅助你实现对输入数据的修剪。
- TrimSpace
TrimSpace
函数去去除输入字符串 / 字节切片首部和尾部的空白字符,它对空白字符的定义与前面 Fields
函数采用的空白字符定义相同:
// go-bytes-and-strings/trim_and_transform.go
// TrimSpace(string)
fmt.Println(strings.TrimSpace("\t\n\f I love Go!! \n\r")) // I love Go!!
fmt.Println(strings.TrimSpace("I love Go!! \f\v \n\r")) // I love Go!!
fmt.Println(strings.TrimSpace("I love Go!!")) // I love Go!!
// TrimSpace([]byte)
fmt.Printf("%q\n", bytes.TrimSpace([]byte("\t\n\f I love Go!! \n\r"))) // "I love Go!!"
fmt.Printf("%q\n", bytes.TrimSpace([]byte("I love Go!! \f\v \n\r"))) // "I love Go!!"
fmt.Printf("%q\n", bytes.TrimSpace([]byte("I love Go!!"))) // "I love Go!!"
- Trim、TrimRight 和 TrimLeft
和 TrimSpace
仅能修剪掉输入数据前后的空白字符相比,Trim
函数允许我们自定义要修剪掉的字符集合:
// Trim、TrimLeft、TrimRight(string)
fmt.Println(strings.Trim("\t\n fffI love Go!!\n \rfff", "\t\n\r f")) // I love Go!!
fmt.Printf("%q\n", strings.TrimLeft("\t\n fffI love Go!!\n \rfff", "\t\n\r f")) // "I love Go!!\n \rfff"
fmt.Printf("%q\n", strings.TrimRight("\t\n fffI love Go!!\n \rfff", "\t\n\r f")) // "\t\n fffI love Go!!"
// Trim、TrimLeft、TrimRight([]byte)
fmt.Printf("%q\n", bytes.Trim([]byte("\t\n fffI love Go!!\n \rfff"), "\t\n\r f")) // I love Go!!
fmt.Printf("%q\n", bytes.TrimLeft([]byte("\t\n fffI love Go!!\n \rfff"), "\t\n\r f")) // "I love Go!!\n \rfff"
fmt.Printf("%q\n", bytes.TrimRight([]byte("\t\n fffI love Go!!\n \rfff"), "\t\n\r f")) // "\t\n fffI love Go!!"
Trim
函数的逻辑很简单,就是从输入数据的首尾两端分别找到第一个不在 “修剪字符集合 (cutset)” 中的字符,然后位于这两个字符中间的内容以及这两个字符的字符序列作为返回值返回;相比于 Trim函数
的首尾兼顾,TrimLeft
仅在输入的首部从左向右找出第一个不在 “修剪字符集合 (cutset)” 中的字符,然后将该字符后面的字符序列连同该字符作为返回值返回;同理,TrimRight
仅在输入的尾部从右向左找出第一个不在 “修剪字符集合 (cutset)” 中的字符,然后将该字符前面的字符序列连同该字符作为返回值返回。
图 9-3-2:Trim、TrimRight 和 TrimLeft 的工作原理
- TrimPrefix 和 TrimSuffix
TrimPrefix
和 TrimSuffix
两个函数分别用于修剪掉输入数据中的前缀字符串和后缀字符串。不过初学者很容易将这两个函数与 TrimLeft
和 TrimRight
弄混。以 TrimPrefix
和 TrimLeft
为例,它们的原型如下:
// $GOROOT/src/strings/strings.go
func TrimLeft(s, cutset string) string
func TrimPrefix(s, prefix string) string
两个函数原型完全一样,但区别在于对第二个参数的理解不同:TrimLeft
的第二个应理解为一个字符的集合,而 TrimPrefix
的第二个参数应理解为一个整体的字符串。通过下面的例子我们就很容易理解这两个函数的差别了:
fmt.Printf("%q\n", strings.TrimLeft("prefix,prefix I love Go!!", "prefix,")) // " I love Go!!"
fmt.Printf("%q\n", strings.TrimPrefix("prefix,prefix I love Go!!", "prefix,")) // "prefix I love Go!!"```
TrimPrefix
将第二个参数 prefix,
当成一个整体字符串在原字符串首部进行查找,找到第一个匹配的字符串后将其剔除掉,返回剩余字符串,即 prefix I love Go!!
;而 TrimLeft
将第二个参数 prefix,
视为字符集合 {'p', 'r', 'e', 'f', 'i', 'x', ','}
,在原输入字符串从左到右查找第一个不在该集合中的字符,即 I
左边的空格,于是将空格及后面的字符串返回,即得到:I love Go!!
。
2) 变换
在处理输入字符串 / 字节切片数据之前对其进行适当的变换也是日常我们经常遇到的情况,比如:大小写转换、替换输入中某些特定字符等。
- 大小写转换
strings
和 bytes
提供了 ToUpper
和 ToLower
函数用于对输入数据进行大写转换和小写转换:
// ToUpper、ToLower(string)
fmt.Printf("%q\n", strings.ToUpper("i LoVe gOlaNg!!")) // "I LOVE GOLANG!!"
fmt.Printf("%q\n", strings.ToLower("i LoVe gOlaNg!!")) // "i love golang!!"
// ToUpper、ToLower([]byte)
fmt.Printf("%q\n", bytes.ToUpper([]byte("i LoVe gOlaNg!!"))) // "I LOVE GOLANG!!"
fmt.Printf("%q\n", bytes.ToLower([]byte("i LoVe gOlaNg!!"))) // "i love golang!!"
- Map 函数
Go 标准库在 strings
和 bytes
包中提供了 Map
函数。顾名思义,该函数用于将原字符串 / 字节切片中的部分数据按照传入的映射规则变换为新数据。下面示例用,我们通过这种方式将原输入数据中的 python
变换为了 golang
:
// Map(string)
trans := func(r rune) rune {
switch {
case r == 'p':
return 'g'
case r == 'y':
return 'o'
case r == 't':
return 'l'
case r == 'h':
return 'a'
case r == 'o':
return 'n'
case r == 'n':
return 'g'
}
return r
}
fmt.Printf("%q\n", strings.Map(trans, "I like python!!")) // "I like golang!!"
// Map([]byte)
fmt.Printf("%q\n", bytes.Map(trans, []byte("I like python!!"))) // "I like golang!!"
6. 快速对接 I/O 模型
Go 语言的整个 I/O
模型都建立在 io.Writer
以及 io.Reader
这两个神奇的接口类型之上,标准库中绝大多数进行 I/O 操作的函数或方法均将它们作为参与 I/O
操作的参数的类型:
// $GOROOT/src/net/http/client.go
func Post(url, contentType string, body io.Reader) (resp *Response, err error)
// $GOROOT/src/net/http/request.go
func NewRequest(method, url string, body io.Reader) (*Request, error)
// $GOROOT/src/net/tcpsock.go
func (c *TCPConn) ReadFrom(r io.Reader) (int64, error)
// $GOROOT/src/log/log.go
func New(out io.Writer, prefix string, flag int) *Logger
// $GOROOT/src/io/io.go
func Copy(dst io.Writer, src io.Reader) (written int64, err error)
而字符串类型与字节切片也常被作为数据源传递给提供 I/O 操作的函数或方法,但我们不能直接将字符串类型 / 字节切片型变量传递给 io.Reader
类型参数。如果每次都要自己实现 io.Reader
接口的 Read
方法又十分繁琐。别急,strings
和 bytes
包提供了快速创建实现 io.Reader
接口的方案。我们通过这两个包的 NewReader
函数并传入我们的数据域即可创建一个满足 io.Reader
接口的实例,见下面示例:
// go-bytes-and-strings/string_and_bytes_reader.go
package main
import (
"bytes"
"fmt"
"io"
"strings"
)
func main() {
var buf bytes.Buffer
var s = "I love Go!!"
_, err := io.Copy(&buf, strings.NewReader(s))
if err != nil {
panic(err)
}
fmt.Printf("%q\n", buf.String()) // "I love Go!!"
buf.Reset()
var b = []byte("I love Go!!")
_, err = io.Copy(&buf, bytes.NewReader(b))
if err != nil {
panic(err)
}
fmt.Printf("%q\n", buf.String()) // "I love Go!!"
}
通过创建的 strings.Reader
或 bytes.Reader
新实例,我们就可以将作为数据源的字符串或字节切片中的数据读出来了。
7. 小结
本节介绍了标准库 strings
包和 bytes
包中主要 API 的使用方法和注意事项,请牢记下面要点:
- 标准库
strings
包和bytes
包提供的 API 具有较高相似性,可相互参酌学习使用; - 按类别了解标准库
strings
包和bytes
包 API 的使用可取的事半功倍的效果; - 理解一些表面相似的 API 的行为差异,比如:
TrimLeft
和TrimPrefix
等。