专栏目录
11 理解包导入路径的含义 44 利其器!Go常用工具大检阅 35 告别乱码!GO语言字符集编码方案间转换 22 Go 并发模型和常见并发模式 12 go语言 init 函数的妙用 41 与时俱进!使用module管理依赖包 1 参考 Go 项目布局设计你的项目结构 16 方法集合决定接口实现 17 go变长参数函数的妙用 7 go语言定义“零值可用”的类型 30 Go 惯例:将测试依赖的外部数据文件放在 testdata 下面 34 一文告诉你如何在 Go 中实现 HTTPS 通信 24 sync 包的正确使用姿势 6 Go“枚举常量”的惯用实现方法 3 Go 标识符的命名惯例 4 变量声明形式尽量保持一致 2 gofmt:Go代码风格的唯一标准 14 defer 让你的代码更清晰 10 Go 字符串是原生类型 8 用复合字面值作初值构造器 9 深入理解和高效运用切片 13 Go 函数是“一等公民”的理解 23 Go channel 的常见使用模式 18 定义小接口是 Go 的惯例 19 不要在函数参数中使用空接口(interface{}) 21 面试必考!掌握 goroutine 的调度原理 15 Go 方法的本质 29 Go 单元测试惯例:表驱动 26 if err != nil 重复太多可以这么办 31 为被测对象建立性能基准 32 掌握 Go 代码性能剖析神器:pprof 25 别笑!这就是 Go 的错误处理哲学 27 不要让 panic 掺和到正常错误处理中 39 慎用reflect包提供的反射能力 38 小心被kill!不要忽略对系统信号的处理 33 掌握 Go 代码调试利器:delve 36 像极!bytes包和strings包的那些相似操作 43 让你的Go包拥有个性化的导入路径 40 与C互操作不是免费的!疑问了解cgo的使用成本 37 time包,你用对了吗 42 小即是美?构建最小Go程序容器镜像 20 要提高代码可测试性,请使用接口 45 未雨绸缪!Go语言常见“坑”大汇 28 一文告诉你测试包的包名要不要带“\_test”后缀 5 无类型常量让代码更简化

36 像极!bytes包和strings包的那些相似操作

九路
• 阅读 576

像极!bytes包和strings包的那些相似操作

对数据类型为字节切片 ([]byte) 或字符串 (string) 的对象的处理是我们在 Go 语言编程过程中最常见的操作。经过前面章节的学习后,我们知道字节切片本质上是一个 “三元组 (array, len, cap)”,而 字符串则是一个 “二元组 (str, len)”(如下图所示):
36 像极!bytes包和strings包的那些相似操作

图 9-3-1:字节切片与字符串的运行时表示

Go 字节切片为内存中的字节序列提供了抽象,而 Go 字符串则代表了采用 UTF-8 编码的 Unicode 字符的数组。Go 标准库中的 bytes 包和 strings 包分别为字节切片和字符串这两种抽象类型提供了基本操作类 API。这里之所以将这两个标准库包放在一起说明,是因为这两个包提供的 API 接口十分相似,在后面的具体 API 使用举例时大家可以感受到这一点。

bytesstrings 包提供的 API 几乎涵盖了所有基本操作,大致可分为如下几类:

  • 查找与替换
  • 比较
  • 拆分
  • 拼接
  • 修剪和变换
  • 快速创建实现了 io.Reader 接口的实例

本节中,我们就来了解一下如何使用这两个包对字节切片和字符串实现上述几类基本操作。一旦熟练掌握了这些常见基本操作的使用方法,处理字节切片和字符串以及由它们组成的复合类型数据对象时就可以游刃有余了。

1. 查找与替换

针对一个字符串 / 字节切片,我们经常做的操作包括查找其中是否存在某一个字符串。如果存在,那么返回该字符串在原字符串 / 字节切片中第一次出现时的位置信息 (下标)。有些时候,我们还会用另外一个字符串 / 字节切片对其进行替换。

1) 定性查找

所谓 “定性查找” 就是指返回有 (true) 和无 (false) 的查找。bytes 包和 strings 提供了一组名字相同的定性查找 API,包括:Contains 系列、HasPrefixHasSuffix。我们看下面用法示例 (示例源码位于 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 类型值传入) 是否包含在第一个参数代表的字符串或字节切片中。

  • HasPrefixHasSuffix 函数
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 函数本质上就等价于最后一个参数传入 -1Replace 函数:
// $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 类型实例化时则可以传入多组 oldnew 参数,这样后续在使用 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  "" == ""
} 

stringsbytes 包还共同提供了 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)

bytesstrings 均提供了 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 标准库的 stringsbytes 提供的对字符串 / 字节切片进行分割的 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
  • SplitAfterSplit 的不同点在于它对原字符串 / 字节切片的分割点在每个分隔符的后面,由于分隔符并未真正起到 “分隔” 的作用,因此它不会被剔除掉,它也会作为子串的一部分返回;SplitAfterN 函数的最后一个参数表示对原字符串进行分割后产生的分段数量,SplitAfter 函数等价于给 SplitAfterN 的最后一个参数传入 -1

4. 拼接 (Concatenate)

拼接是上面 “分割” 的逆过程。stringsbytes 包分别提供了各自的 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 包提供了一系列 TrimAPI 可以辅助你实现对输入数据的修剪。

  • 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)” 中的字符,然后将该字符前面的字符序列连同该字符作为返回值返回。
36 像极!bytes包和strings包的那些相似操作

图 9-3-2:Trim、TrimRight 和 TrimLeft 的工作原理

  • TrimPrefix 和 TrimSuffix

TrimPrefixTrimSuffix 两个函数分别用于修剪掉输入数据中的前缀字符串和后缀字符串。不过初学者很容易将这两个函数与 TrimLeftTrimRight 弄混。以 TrimPrefixTrimLeft 为例,它们的原型如下:

// $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) 变换

在处理输入字符串 / 字节切片数据之前对其进行适当的变换也是日常我们经常遇到的情况,比如:大小写转换、替换输入中某些特定字符等。

  • 大小写转换

stringsbytes 提供了 ToUpperToLower 函数用于对输入数据进行大写转换和小写转换:

// 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 标准库在 stringsbytes 包中提供了 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 方法又十分繁琐。别急,stringsbytes 包提供了快速创建实现 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.Readerbytes.Reader 新实例,我们就可以将作为数据源的字符串或字节切片中的数据读出来了。

7. 小结

本节介绍了标准库 strings 包和 bytes 包中主要 API 的使用方法和注意事项,请牢记下面要点:

  • 标准库 strings 包和 bytes 包提供的 API 具有较高相似性,可相互参酌学习使用;
  • 按类别了解标准库 strings 包和 bytes 包 API 的使用可取的事半功倍的效果;
  • 理解一些表面相似的 API 的行为差异,比如:TrimLeftTrimPrefix 等。
点赞
收藏
评论区
推荐文章

暂无数据