Go 语言标准库之 strings 包
Go 语言的 strings 包实现了字符串的常用操作,本文介绍 strings 包的常用使用。
strings 包常用函数
比较字符串 Compare/EqualFold
// 按照字典序比较两个字符串,a = b 返回 0,a < b 返回 -1,a > b 返回 1
// 通常情况下直接使用 =、>、< 直接比较会更快一些
func Compare(a, b string) int
// 判断两个字符串(不区分大小写)是否相同
func EqualFold(s, t string) bool
示例代码如下:
func main() {
s1, s2 := "aBc", "AbC"
fmt.Println(strings.Compare(s1, s2)) // 1
fmt.Println(s1 == s2) // false
fmt.Println(s1 > s2) // true
fmt.Println(s1 < s2) // false
// 不区分大小写比较是否相同
fmt.Println(strings.EqualFold(s1, s2)) // true
}
是否有指定前/后缀 HasPrefix/HasSuffix
// 判断字符串 s 是否有前缀子串 prefix
func HasPrefix(s, prefix string) bool
// 判断字符串 s 是否有后缀子串 suffix
func HasSuffix(s, suffix string) bool
示例代码如下:
func main() {
fmt.Println(strings.HasPrefix("hello", "he")) // true
fmt.Println(strings.HasPrefix("hello", "eh")) // false
fmt.Println(strings.HasPrefix("hello", "")) // true
fmt.Println(strings.HasSuffix("hello", "lo")) // true
fmt.Println(strings.HasSuffix("hello", "ol")) // false
fmt.Println(strings.HasSuffix("hello", "")) // true
}
是否包含指定子串/字符 Contains/ContainsRune/ContainsAny
// 判断字符串 s 是否包含子串 substr
func Contains(s, substr string) bool
// 判断字符串 s 是否包含字符 r
func ContainsRune(s string, r rune) bool
// 判断字符串 s 是否包含 chars 中的任一字符。如果 chars 为空串,直接返回 false
func ContainsAny(s, chars string) bool
示例代码如下:
func main() {
fmt.Println(strings.Contains("hello", "el")) // true
fmt.Println(strings.Contains("hello", "")) // true
fmt.Println(strings.Contains("", "")) // true
fmt.Println(strings.ContainsRune("hello", 'e')) // true
fmt.Println(strings.ContainsRune("hello", 'a')) // false
fmt.Println(strings.ContainsAny("hello", "eo")) // true
fmt.Println(strings.ContainsAny("hello", "ei")) // true
fmt.Println(strings.ContainsAny("hello", "")) // false
fmt.Println(strings.ContainsAny("", "")) // false
}
计算指定子串数目 Count
// 计算字符串 s 中不重叠的子串 sep 的数目。如果子串 sep 为空,直接返回 len(s) + 1
func Count(s, sep string) int
示例代码如下:
func main() {
fmt.Println(strings.Count("aaa", "a")) // 3
fmt.Println(strings.Count("aaa", "aa")) // 1
fmt.Println(strings.Count("aaa", "")) // 4
}
定位索引 Index/IndexByte/IndexRune/IndexAny/IndexFunc
// 返回子串 sep 在字符串 s 中第一次出现的索引下标,不存在返回 -1
func Index(s, sep string) int
// 返回字节 c 在字符串 s 中第一次出现的索引下标,不存在返回 -1
func IndexByte(s string, c byte) int
// 返回字符 r 在字符串 s 中第一次出现的索引下标,不存在返回 -1
func IndexRune(s string, r rune) int
// 返回 chars 中任一字符在字符串 s 中第一次出现的索引下标,不存在或者 chars 为空串返回 -1
func IndexAny(s, chars string) int
// 返回字符串 s 中第一次满足函数 f 的索引下标(该处的字符 r 满足 f(r) == true),不存在返回 -1
func IndexFunc(s string, f func(rune) bool) int
示例代码如下:
func main() {
fmt.Println(strings.Index("hello world", " wor")) // 5
fmt.Println(strings.Index("hello world", "aaa")) // -1
fmt.Println(strings.IndexByte("hello world", 'l')) // 2
fmt.Println(strings.IndexByte("hello world", 'x')) // -1
fmt.Println(strings.IndexRune("hello world", 'l')) // 2
fmt.Println(strings.IndexRune("hello world", 'x')) // -1
fmt.Println(strings.IndexAny("hello world", "ie")) // 1
fmt.Println(strings.IndexAny("hello world", "mc")) // -1
f := func(c rune) bool {
return c == 'w'
}
fmt.Println(strings.IndexFunc("hello world", f)) // 6
fmt.Println(strings.IndexFunc("hello 世界", f)) // -1
}
定位索引 LastIndex/LastIndexAny/LastIndexByte/LastIndexFunc
// 返回子串 sep 在字符串 s 中最后一次出现的索引下标,不存在返回 -1
func LastIndex(s, sep string) int
// 返回字节 c 在字符串 s 中最后一次出现的索引下标,不存在返回 -1
func LastIndexByte(s string, c byte) int
// 返回 chars 中任一字符在字符串 s 中最后一次出现的索引下标,不存在或者 chars 为空串返回 -1
func IndexAny(s, chars string) int
// 返回字符串 s 中最后一次满足函数 f 的索引下标(该处的字符 r 满足 f(r) == true),不存在返回 -1
func IndexFunc(s string, f func(rune) bool) int
示例代码如下:
func main() {
fmt.Println(strings.LastIndex("hello world", " wor")) // 5
fmt.Println(strings.LastIndex("hello world", "aaa")) // -1
fmt.Println(strings.LastIndexByte("hello world", 'l')) // 9
fmt.Println(strings.LastIndexByte("hello world", 'x')) // -1
fmt.Println(strings.LastIndexAny("hello world", "ie")) // 1
fmt.Println(strings.LastIndexAny("hello world", "mc")) // -1
f := func(c rune) bool {
return c == 'l'
}
fmt.Println(strings.LastIndexFunc("hello world", f)) // 9
fmt.Println(strings.LastIndexFunc("hello 世界", f)) // 3
}
大小写转换 ToLower/ToUpper
// 返回将字符串 s 的所有字母都转为对应的小写的新字符串
func ToLower(s string) string
// 返回将字符串 s 的所有字母都转为对应的大写的新字符串
func ToUpper(s string) string
示例代码如下:
func main() {
fmt.Println(strings.ToLower("ABCdeF")) // abcdef
fmt.Println(strings.ToUpper("abcDEf")) // ABCDEF
}
重复串联 Repeat
// 返回 count 个字符串 s 串联后的新字符串,count 不能传负数
func Repeat(s string, count int) string
示例代码如下:
func main() {
fmt.Println("ba" + strings.Repeat("na", 2)) // banana
}
替换子串 repace/replaceAll
// 返回将字符串 s 中前 n 个不重叠子串 old 都替换为子串 new 的新字符串,如果 n < 0 会替换所有子串 old
func Replace(s, old, new string, n int) string
// 返回将字符串 s 中所有不重叠子串 old 都替换为子串 new 的新字符串,相当于使用 Replace 时n < 0
func ReplaceAll(s, old, new string) string
示例代码如下:
func main() {
fmt.Println(strings.Replace("aaa aaa aaa", "aa", "A", 2)) // Aa Aa aaa
fmt.Println(strings.Replace("aaa aaa aaa", "aa", "A", -1)) // Aa Aa Aa
fmt.Println(strings.ReplaceAll("aaa aaa aaa", "aa", "A")) // Aa Aa Aa
}
字符映射替换 Map
// 返回对字符串 s 中每一个字符 r 执行 mapping(r) 操作后的新字符串
func Map(mapping func(rune) rune, s string) string
示例代码如下:
func main() {
mapping := func(r rune) rune {
if 'a' <= r && r <= 'z' {
return r - 'a' + 'A'
}
return r
}
fmt.Println(strings.Map(mapping, "abcdef")) // ABCDEF
}
去除前后缀 Trim/TrimSpace/TrimFunc
// 返回将字符串 s 前后端所有 cutset 包含的字符都去除的新字符串
func Trim(s string, cutset string) string
// 返回将字符串 s 前后端所有空白字符(unicode.IsSpace 指定)都去除的新字符串
func TrimSpace(s string) string
// 返回将字符串 s 前后端字符 r(满足 f(r) = true)都去除的新字符串
func TrimFunc(s string, f func(rune) bool) string
示例代码如下:
func main() {
fmt.Println(strings.Trim("?!?hello world!?!", "?!")) // hello world
fmt.Println(strings.TrimSpace(" hello world ")) // hello world
f := func(r rune) bool {
if r == '!' || r == '?' {
return true
}
return false
}
fmt.Println(strings.TrimFunc("?!?hello world!?!", f)) // hello world
}
去除前缀 TrimLeft/TrimLeftFunc/TrimPrefix
// 返回将字符串 s 前端所有 cutset 包含的字符都去除的新字符串
func TrimLeft(s string, cutset string) string
// 返回将字符串 s 前端字符 r(满足 f(r) = true)都去除的新字符串
func TrimLeftFunc(s string, f func(rune) bool) string
// 返回将字符串 s 可能的前缀子串 prefix 去除的新字符串
func TrimPrefix(s, prefix string) string
示例代码如下:
func main() {
fmt.Println(strings.TrimLeft("?!?hello world!?!", "?!")) // hello world!?!
f := func(r rune) bool {
if r == '!' || r == '?' {
return true
}
return false
}
fmt.Println(strings.TrimLeftFunc("?!?hello world!?!", f)) // hello world!?!
fmt.Println(strings.TrimPrefix("?!?hello world!?!", "?!?hell")) // o world!?!
}
去除后缀 TrimRight/TrimRightFunc/TrimSuffix
// 返回将字符串 s 后端所有 cutset 包含的字符都去除的新字符串
func TrimRight(s string, cutset string) string
// 返回将字符串 s 后端字符 r(满足 f(r) = true)都去除的新字符串
func TrimRightFunc(s string, f func(rune) bool) string
// 返回将字符串 s 可能的后缀子串 suffix 去除的新字符串
func TrimSuffix(s, suffix string) string
示例代码如下:
func main() {
fmt.Println(strings.TrimRight("?!?hello world!?!", "?!")) // ?!?hello world
f := func(r rune) bool {
if r == '!' || r == '?' {
return true
}
return false
}
fmt.Println(strings.TrimRightFunc("?!?hello world!?!", f)) // ?!?hello world
fmt.Println(strings.TrimSuffix("?!?hello world!?!", "orld!?!")) // ?!?hello w
}
分割字符串 Fields/FieldsFunc
// 返回将字符串按照空白(unicode.IsSpace确定,可以是一到多个连续的空白字符)分割的多个字符串
// 如果字符串全部是空白或者是空字符串的话,会返回空切片
func Fields(s string) []string
// 返回将字符串按照分隔符 r(满足 f(r) == true)分割的多个字符串
// 如果字符串全部是分隔符或者是空字符串的话,会返回空切片
func FieldsFunc(s string, f func(rune) bool) []string
示例代码如下:
func main() {
fmt.Printf("%q\n", strings.Fields(" hello world go ")) // ["hello" "world" "go"]
f := func(r rune) bool {
return !unicode.IsLetter(r) && !unicode.IsNumber(r)
}
fmt.Printf("%q\n", strings.FieldsFunc(" hello world go ", f)) // ["hello" "world" "go"]
}
切割字符串 Split/SplitN/SplitAfter/SplitAfterN
// 用去除每一个 sep 的方式对字符串 s 进行分割,会分割到结尾,返回分割出的所有子串组成的切片
// 每一个 sep 都会进行一次分割,即使两个 sep 相邻,也会进行两次分割
// 如果 sep 空串,Split 会将字符串 s 分割为一个字符一个子串
func Split(s, sep string) []string
// 类似 Split,但是参数 n 决定分割后的切片大小。n < 0:等同 Split(s, sep);n == 0:返回空切片;
// n > 0:最多分割出 n 个子串,最后一个子串包含未进行切割的部分
func SplitN(s, sep string, n int) []string
// 用在每一个 sep 后面切割的方式对字符串 s 进行分割,会分割到结尾,返回分割出的所有子串组成的切片
// 每一个 sep 都会进行一次分割,即使两个 sep 相邻,也会进行两次分割
// 如果 sep 空串,Split 会将字符串 s 分割为一个字符一个子串
func SplitAfter(s, sep string) []string
// 类似 SplitAfter,但是参数 n 决定分割后的切片大小。n < 0:等同 SplitAfter(s, sep);
// n == 0:返回空切片;n > 0:最多分割出 n 个子串,最后一个子串包含未进行切割的部分
func SplitAfterN(s, sep string, n int) []string
示例代码如下:
func main() {
// 通过去除每一个 sep 的方式进行分割,分割出来的子串后面不带 sep
fmt.Printf("%q\n", strings.Split("aAa", "a")) // ["" "A" ""]
// SplitN 指定了返回的切片的长度,切片最后一部分是未被处理的
fmt.Printf("%q\n", strings.SplitN("aAa", "a", 2)) // ["" "Aa"]
fmt.Printf("%q\n", strings.SplitN("aAa", "a", 0)) // []
fmt.Printf("%q\n", strings.SplitN("aAa", "a", -1)) // ["" "A" ""]
// 通过在每一个 sep 后面进行切割的方式进行分割,分割出来的子串后面带有 sep
fmt.Printf("%q\n", strings.SplitAfter("aAa", "a")) // ["a" "Aa" ""]
fmt.Printf("%q\n", strings.SplitAfterN("aAa", "a", 2)) // ["a" "Aa"]
fmt.Printf("%q\n", strings.SplitAfterN("aAa", "a", 0)) // []
fmt.Printf("%q\n", strings.SplitAfterN("aAa", "a", -1)) // ["a" "Aa" ""]
}
连接字符串 Join
// 将一系列字符串连接为一个新的字符串,之间用 sep 来分隔
func Join(elems []string, sep string) string
示例代码如下:
func main() {
ss := []string{"abc", "def", "gh"}
fmt.Println(strings.Join(ss, ",")) // abc,def,gh
}
strings.Builder 使用
在字符串拼接时可以通过strings.Builder
的写入方法来高效构建字符串,它最小化了内存拷贝。
方法介绍
// 预分配内存
func (b *Builder) Grow(n int)
// 返回当前 b 底层用于存储数据的 []byte 切片的长度和容量
func (b *Builder) Len() int
func (b *Builder) Cap() int
// 将当前 b 清空
func (b *Builder) Reset()
// 往当前 b 中写入不同类型的数据,返回写入数据的字节大小和发生的错误
func (b *Builder) Write(p []byte) (int, error)
func (b *Builder) WriteByte(c byte) error
func (b *Builder) WriteRune(r rune) (int, error)
func (b *Builder) WriteString(s string) (int, error)
// 将当前 b 中存储的数据转换为字符串输出
func (b *Builder) String() string
从上面的方法可以看到,strings.Builder
实现了io.Writer
接口:
type Writer interface {
Write(p []byte) (n int, err error)
}
简单代码使用如下:
func main() {
var b strings.Builder
// 四种写入方法
b.Write([]byte("hello"))
b.WriteByte(' ')
b.WriteRune('您')
b.WriteString("好")
for i := 1; i <= 3; i++ {
// strings.Builder 实现了 io.Writer 接口
fmt.Fprintf(&b, "%d...", i)
}
fmt.Println(b.String()) // hello 您好1...2...3...
fmt.Println(b.Len()) // 24
fmt.Println(b.Cap()) // 48
}
底层分析
?? 存储结构
strings.Builder
底层是通过内部的[]byte
来存储数据:
type Builder struct {
addr *Builder // of receiver, to detect copies by value
buf []byte
}
当调用写入方法的时候,数据实际上是被追加(append)到[]byte
上:
func (b *Builder) Write(p []byte) (int, error) {
b.copyCheck()
b.buf = append(b.buf, p...)
return len(p), nil
}
由于底层是 Slice,所以写入时可能会导致 Slice 扩容,所以strings.Builder
提供了Grow()
方法预分配内存,避免多次扩容。Grow()
方法的具体实现如下:
func (b *Builder) grow(n int) {
buf := make([]byte, len(b.buf), 2*cap(b.buf)+n)
copy(buf, b.buf)
b.buf = buf
}
func (b *Builder) Grow(n int) {
b.copyCheck()
if n < 0 {
panic("strings.Builder.Grow: negative count")
}
if cap(b.buf)-len(b.buf) < n {
b.grow(n)
}
}
Grow()
方法保证了其内部的 Slice 一定能够写入 n 个字节,只有当 Slice 剩余空间不足以写入 n 个字节时,扩容才会发生。
?? 不允许被拷贝
strings.Builder
不允许被拷贝,当试图拷贝strings.Builder
并写入的时候,程序会报错:
func main() {
var b1 strings.Builder
b1.WriteString("aaa")
b2 := b1
b2.WriteString("bbb")
fmt.Println(b2.String())
}
// panic: strings: illegal use of non-zero Builder copied by value
strings.Builder
结构体有一个指向*Builder
的指针 add,在调用写入方法之后,该指针会指向自己:
func (b *Builder) Write(p []byte) (int, error) {
b.copyCheck()
//...
}
func (b *Builder) copyCheck() {
if b.addr == nil {
// This hack works around a failing of Go's escape analysis
// that was causing b to escape and be heap allocated.
// See issue 23382.
// TODO: once issue 7921 is fixed, this should be reverted to
// just "b.addr = b".
b.addr = (*Builder)(noescape(unsafe.Pointer(b)))
} else if b.addr != b {
panic("strings: illegal use of non-zero Builder copied by value")
}
}
而我们执行b1 = b2
时,结构体内部同样拷贝了指向 b1 的指针 add,也就是说b2.add = &b1
。所以,当对 b2 进行写入操作时,会再次进入copyCheck()
方法,直接报 panic 错误。
对于一个空strings.Builder
的拷贝是允许的,因为此时 add 指针为 nil,执行copyCheck()
时不会进行b.add != b
的判断条件中。
func main() {
var b1 strings.Builder
fmt.Println(b1) // { []}
b2 := b1
b2.WriteString("aaa")
fmt.Println(b2) // {0xc0000cdf30 [97 97 97]}
}
?? String()
strings.Builder
返回当前数据的字符串时,为了节省内存分配,它通过使用指针技术将内部的[]byte
转换为字符串,所以String()
方法在转换的时候节省了时间和空间。其具体实现方式如下:
func (b *Builder) String() string {
return *(*string)(unsafe.Pointer(&b.buf))
}
?? 不支持并发
strings.Builder
不支持并发读写,是不安全的,所以最好在单协程中使用。如果strings.Builder
支持并发,下面代码运行结果应该是 1000:
func main() {
var b strings.Builder
var wait sync.WaitGroup
for n := 0; n < 10000; {
wait.Add(1)
go func() {
b.WriteString("1")
n++
wait.Done()
}()
}
wait.Wait()
fmt.Println(b.Len()) // 9349
}
参考
- Go中strings的常用方法详解
- Golang 中 strings.builder 的 7 个要点
- strings.Builder 源码分析