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
}

参考

  1. Go中strings的常用方法详解
  2. Golang 中 strings.builder 的 7 个要点
  3. strings.Builder 源码分析