Go 语言
标准库、反射和 cgo
bytes 和 strings 的用法

bytes 和 strings 的用法

对数据类型为字节切片 []byte 或 字符串 string 的对象的处理是 Go 语言编程过程中最常见的操作。

我们知道字节切片本质上是一个三元组(array、len、cap),而字符串则是一个二元组(str,len)。Go 标准库中的 bytes 包和 strings 包分别提供了这两种抽象类型的基本操作类 API。 之所以将这两个标准库放在一起说明,是因为它们提供的 API 十分相似。

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

  • 查找与替换
  • 比较
  • 分割
  • 拼接
  • 修剪和变换
  • 快速对接 I/O 模型

查找与替换

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

定性查找

所谓定性查找就是指返回有 true 和 无 false 的查找。bytes 包和 strings 包都提供了一组名字相同的定向查找 API,包括 Contains 系列、HasPrefix 和 HasSuffix。

Contains 函数

println(strings.Contains("seafood", "foo")) // true
println(strings.Contains("seafood", "bar")) // false
println(strings.Contains("seafood", ""))    // true
println(strings.Contains("", ""))           // true
 
println(bytes.Contains([]byte("seafood"), []byte("foo"))) // true
println(bytes.Contains([]byte("seafood"), []byte("bar"))) // false
println(bytes.Contains([]byte("seafood"), []byte("")))    // true
println(bytes.Contains([]byte("seafood"), nil))           // true
println(bytes.Contains([]byte(""), []byte{}))             // true
println(bytes.Contains(nil, nil)                          // true

值得注意的是,在这个函数语义中,任意字符串都包含空串,任意字节切片也都包含空字节切片以及 nil 切片。

ContainsAny 函数

ContainsAny 函数用于判断字符串中是否包含参数字符串中的任意一个字符。可以理解为两个集合是否存在交集,如果存在则返回 true。

println(strings.ContainsAny("team", "i"))        // false
println(strings.ContainsAny("Golang", "python")) // true
println(strings.ContainsAny("Golang", ""))       // false
println(strings.ContainsAny("", ""))             // false
 
println(bytes.ContainsAny([]byte("team"), "i"))        // false
println(bytes.ContainsAny([]byte("Golang"), "python")) // true
println(bytes.ContainsAny([]byte("Golang"), ""))       // false
println(bytes.ContainsAny(nil, ""))                    // false

ContainsRune 函数

ContainsRune 函数用于判断字符串中是否包含指定的 Unicode 字符。

println(strings.ContainsRune("Go语言", 97))         // true,97 是 a 的 Unicode 编码
println(strings.ContainsRune("Go语言", rune('中'))) // false
 
println(bytes.ContainsRune([]byte("Go语言"), 97))         // true
println(bytes.ContainsRune([]byte("Go语言"), rune('中'))) // false

HasPrefix 函数和 HasSuffix 函数

HasPrefix 函数用于判断字符串是否以指定的前缀开头,HasSuffix 函数用于判断字符串是否以指定的后缀结尾。

println(strings.HasPrefix("Golang", "Go")) // true
println(strings.HasPrefix("Golang", "go")) // false
println(strings.HasPrefix("Golang", ""))   // true
println(strings.HasPrefix("", ""))         // true
 
println(strings.HasSuffix("Golang", "lang")) // true
println(strings.HasSuffix("Golang", "Lang")) // false
println(strings.HasSuffix("Golang", ""))     // true
println(strings.HasSuffix("", ""))           // true
 
println(bytes.HasPrefix([]byte("Golang"), []byte("Go"))) // true
println(bytes.HasPrefix([]byte("Golang"), []byte("go"))) // false
println(bytes.HasPrefix([]byte("Golang"), []byte{}))     // true
println(bytes.HasPrefix([]byte("Golang"), nil))          // true
println(bytes.HasPrefix(nil, nil))                       // true
 
println(bytes.HasSuffix([]byte("Golang"), []byte("lang"))) // true
println(bytes.HasSuffix([]byte("Golang"), []byte("Lang"))) // false
println(bytes.HasSuffix([]byte("Golang"), []byte{}))       // true
println(bytes.HasSuffix([]byte("Golang"), nil))            // true
println(bytes.HasSuffix(nil, nil))                         // true

空串是任何字符串的前缀和后缀,空字节切片和 nil 切片也是任何字节切片的前缀和后缀。

定位查找

定位查找相关函数会给出第一次出现的位置(下标),如果没有找到则返回 -1。另外定位查找还有方向性,从左到右为正向定位查找(Index系列),反之为反向定位查找(LastIndex系列)。

// 定位查找 string
println(strings.Index("chicken", "ken")) // 4
println(strings.Index("chicken", "dmr")) // -1
println(strings.Index("chicken", "c"))   // 0
println(strings.Index("chicken", ""))    // 0
println(strings.IndexAny("chicken", "aeiouy")) // 2
println(strings.IndexRune("chicken", rune('i'))) // 2
 
// 定位查找 []byte
println(bytes.Index([]byte("chicken"), []byte("ken"))) // 4
println(bytes.Index([]byte("chicken"), []byte("dmr"))) // -1
println(bytes.Index([]byte("chicken"), []byte("c")))   // 0
println(bytes.Index([]byte("chicken"), nil))           // 0
println(bytes.IndexAny([]byte("chicken"), "ken"))      // 4
println(bytes.IndexRune([]byte("chicken"), rune('i'))) // 2
 
// 反向定位查找 string
println(strings.LastIndex("go gopher", "er"))     // 10
println(strings.LastIndex("go gopher", "rodent")) // -1
println(strings.LastIndex("go gopher", ""))       // 9
println(strings.LastIndexAny("go gopher", "go"))  // 3
 
// 反向定位查找 []byte
println(bytes.LastIndex([]byte("go gopher"), []byte("er")))     // 10
println(bytes.LastIndex([]byte("go gopher"), []byte("rodent"))) // -1
println(bytes.LastIndex([]byte("go gopher"), nil))              // 9
println(bytes.LastIndexAny([]byte("go gopher"), "go"))          // 3

和 ContainsAny 只查看交集是否为空不同,IndexAny 函数会返回第一个匹配的字符的位置。 另外注意,反向查找空串或 nil 切片,返回的是第一个参数的长度。

strings 包并未提供模糊查询功能,基于正则表达式的模糊查找可以使用标准库 regexp 包。

替换

strings 包和 bytes 包都提供了 Replace 函数用于替换字符串或字节切片中的指定子串。

// 替换 string
println(strings.Replace("oink oink oink", "k", "ky", 2)) // oinky oinky oink
println(strings.Replace("oink oink oink", "oink", "moo", -1)) // moo moo moo
println(strings.Replace("oink", "", "moo", -1)) // mooomooimoonmookmoo
println(strings.ReplaceAll("oink oink oink", "oink", "moo")) // moo moo moo
replacer := strings.NewReplacer("oink", "moo", "m", "n")
println(replacer.Replace("oink oink oink")) // moonk moonk moonk
 
// 替换 []byte
println(string(bytes.Replace([]byte("oink oink oink"), []byte("k"), []byte("ky"), 2))) // oinky oinky oink
println(string(bytes.Replace([]byte("oink oink oink"), []byte("oink"), []byte("moo"), -1)) // moo moo moo
println(string(bytes.Replace([]byte("oink"), nil, []byte("moo"), -1)) // mooomooimoonmookmoo
println(string(bytes.ReplaceAll([]byte("oink oink oink"), []byte("oink"), []byte("moo"))) // moo moo moo

Replace 函数的最后一个参数是一个整数,表示替换的次数。如果为 -1,则表示替换所有匹配的子串。

比较

等值比较

切片类型变量之间不能通过操作符进行等值比较,但可以与 nil 做比较:

var a, b []byte
c := []byte("123")
println(a == nil) // true
println(b == nil) // true
println(a == b)   // true
println(c != nil) // true

string 类型变量原生支持使用操作符进行等值比较,因此不需要像 bytes 包一样提供 Equal 函数,但其也是基于原生字符串类型的等值比较实现的:

// src/bytes/bytes.go
func Equal(a, b []byte) bool {
    return string(a) == string(b)
}

strings 和 bytes 包还共同提供了 EqualFold 函数,用于忽略大小写的字符串比较。

println(strings.EqualFold("Go", "go")) // true
println(bytes.Equal([]byte("Go"), []byte("go"))) // false
println(bytes.EqualFold([]byte("Go"), []byte("go"))) // true

排序比较

bytes 包和 strings 包都提供了 Compare 函数,用于比较两个字符串或字节切片的大小。 但是 Go 原生就是支持通过操作符对字符串类型变量进行排序比较,所以实际应用中,很少会直接使用 strings.Compare 函数。

我们来看一下 bytes 包的 Compare 函数:

func main() {
    var a = []byte{'a', 'b', 'c'}
    var b = []byte{'a', 'b', 'd'
    var c = []byte{}
    var d []byte
 
    println(bytes.Compare(a, b)) // -1
    println(bytes.Compare(b, a)) // 1
    println(bytes.Compare(c, d)) // 0
    println(bytes.Compare(c, nil)) // 0
    println(bytes.Compare(d, nil)) // 0
    println(bytes.Compare(nil, nil)) // 0
}

分割

日常开发中我们需要对 ”1,2,3“ 这种数据进行分割,得到 [1,2,3]。strings 包和 bytes 包都提供了 Split 函数用于分割字符串或字节切片。

Fields 函数

Fields 函数用于将字符串按空白字符分割,返回一个切片。

println(strings.Fields("  foo bar  baz   ")) // [foo bar baz]

Fields 函数会将连续的空白字符当做一个空白字符处理。

Fields 函数采用了 Unicode 空白字符的定义,下面的字符均会被识别为空白字符:

// src/unicode/graphic.go
'\t', '\n', '\v', '\f', '\r', ' ', U+0085 (NEL), U+00A0 (NBSP)

从例子中,我们看到 Fields 函数会忽略输入数据前后的空白字符,如果输入数据仅包含空白字符,则返回空切片。

Split 函数

我们不仅可以使用空白对字符串进行分割外,还可以使用任意字符对字符串进行分割。Split 函数用于将字符串按指定的字符分割,返回一个切片。

// strings.Split
println(strings.Split("a,b,c", ","))  // ["a", "b", "c"]
println(strings.Split("a,b,c", "b"))  // ["a,", ",c"]
println(strings.Split("a,b,c", ""))   // ["a", ",", "b", ",", "c"]
println(strings.Split("a,b,c", "ed")) // ["a,b,c"]
 
println(strings.SplitN("a,b,c", ",", 2)) // ["a", "b,c"]
println(strings.SplitN("a,b,c", ",", 3)) // ["a", "b", "c"]
 
println(strings.SplitAfter("a,b,c", ","))  // ["a,", "b,", "c"]
println(strings.SplitAfterN("a,b,c", ",", 2)) // ["a,", "b,c"]
 
// bytes.Split
println(bytes.Split([]byte("a,b,c"), []byte(",")))  // ["a", "b", "c"]
println(bytes.Split([]byte("a,b,c"), []byte("b")))  // ["a,", ",c"]
println(bytes.Split([]byte("a,b,c"), nil))          // ["a", ",", "b", ",", "c"]
println(bytes.Split([]byte("a,b,c"), []byte("ed")))  // ["a,b,c"]
 
println(bytes.SplitN([]byte("a,b,c"), []byte(","), 2)) // ["a", "b,c"]
println(bytes.SplitN([]byte("a,b,c"), []byte(","), 3)) // ["a", "b", "c"]
 
println(bytes.SplitAfter([]byte("a,b,c"), []byte(",")))  // ["a,", "b,", "c"]
println(bytes.SplitAfterN([]byte("a,b,c"), []byte(","), 2)) // ["a,", "b,c"]

拼接

strings 包和 bytes 包都提供了 Join 函数用于将字符串或字节切片的拼接。

s := []string{"foo", "bar", "baz"}
println(strings.Join(s, ", ")) // foo, bar, baz
 
b := [][]byte{[]byte("foo"), []byte("bar"), []byte("baz")}
println(string(bytes.Join(b, []byte(", ")))) // foo, bar, baz

strings 包还提供了 Builder 类型及相关方法用于高效地拼接字符串。而 bytes 与之对应的是 Buffer 类型及相关方法。

s := []string{"foo", "bar", "baz"}
var builder strings.Builder
for i, str := range s {
    builder.WriteString(str)
    if i != len(s)-1 {
        builder.WriteString(", ")
    }
}
println(builder.String()) // foo, bar, baz
 
b := [][]byte{[]byte("foo"), []byte("bar"), []byte("baz")}
var buffer bytes.Buffer
for i, str := range b {
    buffer.Write(str)
    if i != len(b)-1 {
        buffer.WriteString(", ")
    }
}
println(buffer.String()) // foo, bar, baz

修剪与变换

修剪

在处理输入的数据之前,我们经常会对其进行修剪,比如去除输入输入数据前后的空白字符,去掉特定后缀信息等。 Go 标准库中的 strings 包和 bytes 包都提供了 Trim 系列函数用于修剪字符串或字节切片。

1、TrimSpace 函数用于去除字符串前后的空白字符。

// strings.TrimSpace
println(strings.TrimSpace(" \t\n a lone gopher \n\t\r\n")) // a lone gopher
println(strings.TrimSpace(" \t\n \n\t\r\n")) // ""
 
// bytes.TrimSpace
println(string(bytes.TrimSpace([]byte(" \t\n a lone gopher \n\t\r\n")))) // a lone gopher
println(string(bytes.TrimSpace([]byte(" \t\n \n\t\r\n"))) // ""

2、Trim、TrimLeft 和 TrimRight 函数用于去除字符串前后、左侧和右侧的指定字符。

// strings.Trim
println(strings.Trim("¡¡¡Hello, Gophers!!!", "!¡")) // Hello, Gophers
println(strings.Trim("¡¡¡Hello, Gophers!!!", "¡"))  // Hello, Gophers!!!
 
// bytes.Trim
println(string(bytes.Trim([]byte("¡¡¡Hello, Gophers!!!"), "!¡"))) // Hello, Gophers
println(string(bytes.Trim([]byte("¡¡¡Hello, Gophers!!!"), "¡")))  // Hello, Gophers!!!

Trim 函数逻辑很简单,就是从输入数据的收尾两端分别找出第一个不在修剪字符集合中的字符,然后返回这两个字符之间的子串。 TrimLeft 和 TrimRight 函数的逻辑类似,只是分别从左侧和右侧开始查找。

3、TrimPrefix 和 TrimSuffix 函数用于去除字符串的前缀和后缀。

// strings.TrimPrefix
println(strings.TrimPrefix("Goodbye,, world!", "Goodbye,")) // world!
println(strings.TrimPrefix("Hello, world!", "Goodbye,"))    // Hello, world!
 
// strings.TrimSuffix
println(strings.TrimSuffix("Hello, world!", "world!")) // Hello,
println(strings.TrimSuffix("Hello, world!", "world"))  // Hello, world!
 
// bytes.TrimPrefix
println(string(bytes.TrimPrefix([]byte("Goodbye,, world!"), []byte("Goodbye,")))) // world!
println(string(bytes.TrimPrefix([]byte("Hello, world!"), []byte("Goodbye,"))))    // Hello, world!
 
// bytes.TrimSuffix
println(string(bytes.TrimSuffix([]byte("Hello, world!"), []byte("world!")))) // Hello,
println(string(bytes.TrimSuffix([]byte("Hello, world!"), []byte("world"))))  // Hello, world!

变换

1、大小写转换

strings 包和 bytes 包都提供了 ToUpper 和 ToLower 函数用于将字符串或字节切片中的字符转换为大写或小写。

// strings.ToUpper
println(strings.ToUpper("Gopher")) // GOPHER
println(strings.ToUpper("go gopher")) // GO GOPHER
 
// bytes.ToUpper
println(string(bytes.ToUpper([]byte("Gopher")))) // GOPHER
println(string(bytes.ToUpper([]byte("go gopher")))) // GO GOPHER

2、Map 函数

strings 包和 bytes 包都提供了 Map 函数用于将字符串或字节切片中的字符按照指定的映射规则进行变换。

// strings.Map
println(strings.Map(func(r rune) rune {
    if r == 'o' {
        return 'O'
    }
    return r
}, "go gopher")) // gO gOpher
 
// bytes.Map
println(string(bytes.Map(func(r rune) rune {
    if r == 'o' {
        return 'O'
    }
    return r
}, []byte("go gopher"))) // gO gOpher

快速对接 I/O 模型

Go 语言的整个 I/O 模型都是建立在 io.Reader 和 io.Writer 接口之上的。bytes 包和 strings 包都提供了 Reader 和 Writer 类型,用于快速对接 I/O 模型。

func main() {
    var buf bytes.Buffer
    var s = "Hello, Gophers!"
 
    _, err := io.Copy(&buf, strings.NewReader(s))
    if err != nil {
        log.Fatal(err)
    }
    println(buf.String()) // Hello, Gophers!
 
    buf.Reset()
    var b = []byte("Hello, Gophers!")
    _, err = io.Copy(&buf, bytes.NewReader(b))
    if err != nil {
        log.Fatal(err)
    }
    println(buf.String()) // Hello, Gophers!
}

通过创建 Reader 实例,我们可以将字符串或字节切片转换为 io.Reader 接口类型,然后通过 io.Copy 函数将其写入到 io.Writer 接口类型的实例中。