1. 字符操作
strings 和 bytes 提供了 go 语言中的字符操作,因为还不不支持泛型,所以这两个包提供了几乎一样的函数和类型,区别仅仅在于一个操作 string,其中大致可分为如下几类:
- 查找与替换;
- 比较;
- 拆分;
- 拼接;
- 修剪和变换;
- 快速创建实现了io.Reader接口的实例
2. 查找与替换
2.1 定性查找
所谓“定性查找”就是指返回有(true)和无(false)的查找。strings/bytes 提供 3 类 API,包括 Contains系列、HasPrefix和HasSuffix。
Contains 函数
Contains函数返回的是第一个参数代表的字符串/字节切片中是否包含第二个参数代表的字符串/字节切片
1
2
3
4
5
6
7
8
9
10
|
fmt.Println(strings.Contains("Golang", "Go")) // true
fmt.Println(strings.Contains("Golang", "go")) // false
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(""))) // true
fmt.Println(bytes.Contains([]byte("Golang"), nil)) // true
fmt.Println(bytes.Contains([]byte("Golang"), []byte{})) // true
fmt.Println(bytes.Contains(nil, nil)) // true
|
在这个函数的语义中,任意字符串都包含空串(""),任意字节切片也都包含空字节切片([]byte{})及nil切片。
ContainsAny 函数
ContainsAny函数的语义是,将其两个参数看成两个Unicode字符的集合,如果两个集合存在不为空的交集,则返回true
1
2
3
4
5
6
7
8
9
|
fmt.Println(strings.ContainsAny("Golang", "java")) // 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 注意
|
ContainsRune 函数
ContainsRune用于判断某一个Unicode字符(以码点形式即rune类型值传入)是否包含在第一个参数代表的字符串或字节切片中。
1
2
3
4
5
|
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('中'))) // f
|
HasPrefix 和 HasSuffix 函数
HasPrefix函数用于判断第二个参数代表的字符串/字节切片是不是第一个参数的前缀,同理,HasSuffix函数则用于判断第二个参数是不是第一个参数的后缀。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
fmt.Println(strings.HasPrefix("Golang", "Go")) // true
fmt.Println(strings.HasPrefix("Golang", "lang")) // false
fmt.Println(strings.HasPrefix("Golang", "")) // true
fmt.Println(strings.HasPrefix("", "")) // true
fmt.Println(strings.HasSuffix("Golang", "Golang")) // 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{})) // true
fmt.Println(bytes.HasPrefix([]byte("Golang"), nil)) // true
fmt.Println(bytes.HasPrefix(nil, nil)) // 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
|
要注意的是,在这两个函数的语义中,空字符串("")是任何字符串的前缀和后缀,空字节切片([]byte{})和nil切片也是任何字节切片的前缀和后缀。
2.2 定位查找
定位相关查找函数会给出第二个参数代表的字符串/字节切片在第一个参数中第一次出现的位置(下标),如果没有找到,则返回-1。另外定位查找还有方向性,从左到右为正向定位查找(Index系列),反之为反向定位查找(LastIndex系列)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
// 定位查找(string)
fmt.Println(strings.Index("Learn Golang, Go!", "Go")) // 6
fmt.Println(strings.Index("Learn Golang, Go!", "")) // 0
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.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.LastIndexAny("Learn Golang, Go!", "Java")) // 9
// 反向定位查找([]byte)
fmt.Println(bytes.LastIndex([]byte("Learn Golang, Go!"), nil)) // 17
fmt.Println(bytes.LastIndexAny([]byte("Learn Golang, Go!"), "Java")) // 9
|
IndexAny函数返回非空交集中第一个字符在第一个参数中的位置信息。另外要注意,反向查找空串或nil切片,返回的是第一个参数的长度,但作为位置(下标)信息,这个值已经越界了。
2.3 替换
strings包中提供了两种进行字符串替换的方法:Replace函数与Replacer类型。bytes包中则只提供了Replace函数用于字节切片的替换。
- Replace 最后一个参数是一个整型数,用于控制替换的次数。如果传入-1,则全部替换。
- ReplaceAll函数本质上等价于最后一个参数传入-1的Replace函数
- Replacer类型实例化时则可以传入多组old和new参数,这样后续在使用Replacer.Replace方法对原字符串进行替换时,可以一次实施多组不同字符串的替换。
1
2
3
4
5
6
7
8
|
// $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)
}
|
下面是使用示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
// 替换(string)
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, 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!!
|
当参数old传入空字符串"“或nil(仅字节切片)时,Replace会将new参数所表示的要替入的字符串/字节切片插入原字符串/字节切片的每两个字符(字节)间的“空隙”中。当然原字符串/字节切片的首尾也会被各插入一个new参数值。
3. 比较
3.1 等值比较
根据Go语言规范,切片类型变量之间不能直接通过操作符进行等值比较,但可以与nil做等值比较。但Go语言原生支持通过操作符==或!=对string类型变量进行等值比较,因此strings包未像bytes包一样提供Equal函数。而bytes包的Equal函数的实现也是基于原生字符串类型的等值比较的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
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")
}
}
// $GOROOT/src/bytes/bytes.go
func Equal(a, b []byte) bool {
// Go编译器会为上面这个函数实现中的显式类型转换提供默认优化,不会额外为显式类型转换分配内存空间。
return string(a) == string(b)
}
fmt.Println(bytes.Equal([]byte{'a', 'b', 'c'}, []byte{'a', 'b', 'd'}))
// false "abc" != "abd"
|
strings和bytes包还共同提供了 EqualFold 函数,用于进行不区分大小写的Unicode字符的等值比较。字节切片在比较时,切片内的字节序列将被解释成字符的UTF-8编码表示后再进行比较:
1
2
3
|
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.2 排序比较
bytes包和strings包均提供了Compare方法来对两个字符串/字节切片做排序比较。但Go原生支持通过操作符>、>=、<和<=对字符串类型变量进行排序比较,因此strings包中Compare函数存在的意义更多是为了与bytes包尽量保持API一致,其自身也是使用原生排序比较操作符实现的:
1
2
3
4
5
6
7
8
9
10
|
// $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 的使用示例:
1
2
3
4
5
6
7
8
9
10
11
|
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)
|
2.3 分割
字符串/字节切片分割使用 Split API。
Fields相关函数
Fields 使用空白分割字符串,Fields函数采用了Unicode空白字符的定义,下面的字符均会被识别为空白字符:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
// $GOROOT/src/unicode/graphic.go
'\t', '\n', '\v', '\f', '\r', ' ', U+0085 (NEL), U+00A0 (NBSP)
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会忽略输入数据前后的空白字符以及中间连续的空白字符;如果输入数据仅包含空白字符,那么该函数将返回一个空的string类型切片。
Go标准库还提供了灵活定制分割逻辑的FieldsFunc函数,通过传入一个用于指示是否为“空白”字符的函数,我们可以实现按自定义逻辑对原字符串进行分割:
1
2
3
4
5
6
7
8
|
// go-bytes-and-strings/split_and_fields.go
splitFunc := func(r rune) bool {
return r == rune('\n')
}
// ["\tgo \f \u0085 \u00a0 java " "\rpython"]
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))
|
Split 相关函数
Split相关函数,可以使用更通用的字符对字符串或字节切片进行分割:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
// 使用Split相关函数分割字符串
fmt.Printf("%q\n", strings.Split("a,b,c", ",")) // ["a" "b" "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.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("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.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函数当传入空串(或bytes.Split被传入nil切片)作为分隔符时,Split函数会按UTF-8的字符编码边界对Unicode进行分割,即每个Unicode字符都会被视为一个分割后的子字符串。如果原字符串中没有传入的分隔符,那么Split会将原字符串作为返回的字符串切片中的唯一元素。
- SplitN函数的最后一个参数表示对原字符串进行分割后产生的分段数量,Split函数等价于SplitN函数的最后一个参数被传入-1。
- SplitAfter不同于Split的地方在于它对原字符串/字节切片的分割点在每个分隔符的后面,由于分隔符并未真正起到分隔的作用,因此它不会被剔除,而会作为子串的一部分返回。
- SplitAfterN函数的最后一个参数表示对原字符串进行分割后产生的分段数量,SplitAfter函数等价于SplitAfterN函数的最后一个参数被传入-1。
2.4 拼接
拼接(Concatenate)是分割的逆过程,使用 Join 函数。
1
2
3
4
|
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"
|
string.Builder 和 bytes.Buffer 用于高效地构建字符串:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
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
|
2.5 修剪
去除输入数据中首部和尾部多余的空白、去掉特定后缀信息等使用 Trip 相关函数:
- TrimSpace: 去除输入字符串/字节切片首部和尾部的空白字符(对空白字符的定义通 Fields 函数)
- Trim: 从输入数据的首尾两端分别找到第一个不在修剪字符集合(cutset)中的字符,然后将位于这两个字符中间的内容连同这两个字符作为返回值返回
- TrimRight和TrimLeft 通 Trim,不过一个去除右侧,一个去除左侧
- TrimPrefix和TrimSuffix: 修剪输入数据中的前缀字符串和后缀字符串
1
2
3
4
5
6
7
8
9
10
11
|
// TrimSpace
fmt.Println(strings.TrimSpace("\t\n\f I love Go!! \n\r")) // I love Go!!
fmt.Printf("%q\n", bytes.TrimSpace([]byte("\t\n\f I love Go!! \n\r"))) // "I love Go!!"
// Trim、TrimRight和TrimLeft
fmt.Println(strings.Trim("\t\n fffI love Go!!\n \rfff", "\t\n\r f"))
// I love Go!!
// $GOROOT/src/strings/strings.go
func TrimLeft(s, cutset string) string
func TrimPrefix(s, prefix string) string
|
注意 Trim 的第二个参数应理解为一个字符的集合,而TrimPrefix的第二个参数应理解为一个整体的字符串。
2.6 变换
大小写转换
ToUpper和ToLower函数:
1
2
|
strings.ToUpper("i LoVe gOlaNg!!")
strings.ToLower("i LoVe gOlaNg!!")
|
Map函数
Map 于将原字符串/字节切片中的部分数据按照传入的映射规则变换为新数据:
1
2
3
4
5
6
7
8
9
|
trans := func(r rune) rune {
switch {
case r == 'p':
return 'g'
}
return r
}
fmt.Printf("%q\n", strings.Map(trans, "I like python!!"))
fmt.Printf("%q\n", bytes.Map(trans, []byte("I like python!!")))
|
2.7 快速对接I/O模型
标准库中大多数的 I/O操作都以 io.Writer/io.Reader这两个接口类型作为参数,字符串类型/字节切片没有实现 io.Reader 接口,不能直接作为这些 I/O 操作的入参。strings/bytes 的 NewReader 函数可以讲字符串/字节切片保包装成满足 io.Reader 接口的可读数据源。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
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!!"
}
|