golang-strings— 字符串操作

strings — 字符串操作

字符串常见操作有:

  • 字符串长度;
  • 求子串;
  • 是否存在某个字符或子串;
  • 子串出现的次数(字符串匹配);
  • 字符串分割(切分)为[]string;
  • 字符串是否有某个前缀或后缀;
  • 字符或子串在字符串中首次出现的位置或最后一次出现的位置;
  • 通过某个字符串将[]string 连接起来;
  • 字符串重复几次;
  • 字符串中子串替换;
  • 大小写转换;
  • Trim 操作;
  • … 前面已经说过,由于 string 类型可以看成是一种特殊的 slice 类型,因此获取长度可以用内置的函数 len;同时支持 切片 操作,因此,子串获取很容易。 其他的字符串常见操作就是我们这小节要介绍的,由于这些操作函数的使用比较简单,只会对某些函数举例说明;但会深入这些函数的内部实现,更好的掌握它们。 说明:这里说的字符,指得是 rune 类型,即一个 UTF-8 字符(Unicode 代码点)。

2.1.1 字符串比较

    // Compare 函数,用于比较两个字符串的大小,如果两个字符串相等,返回为 0。如果 a 小于 b ,返回 -1 ,反之返回 1 。不推荐使用这个函数,直接使用 == != > < >= <= 等一系列运算符更加直观。
   func Compare(a, b string) int 
   //   EqualFold 函数,计算 s 与 t 忽略字母大小写后是否相等。
   func EqualFold(s, t string) bool

示例:

a := "gopher"
b := "hello world"
fmt.Println(strings.Compare(a, b))
fmt.Println(strings.Compare(a, a))
fmt.Println(strings.Compare(b, a))

fmt.Println(strings.EqualFold("GO", "go"))
fmt.Println(strings.EqualFold("壹", "一"))

输出结果:

-1
0
1
true
false

2.1.2 是否存在某个字符或子串

有三个函数做这件事:

// 子串 substr 在 s 中,返回 true
func Contains(s, substr string) bool
// chars 中任何一个 Unicode 代码点在 s 中,返回 true
func ContainsAny(s, chars string) bool
// Unicode 代码点 r 在 s 中,返回 true
func ContainsRune(s string, r rune) bool

这里对 ContainsAny 函数进行一下说明,看如下例子:

fmt.Println(strings.ContainsAny("team", "i"))
fmt.Println(strings.ContainsAny("failure", "u & i"))
fmt.Println(strings.ContainsAny("in failure", "s g"))
fmt.Println(strings.ContainsAny("foo", ""))
fmt.Println(strings.ContainsAny("", ""))

输出:

false
true
true
false
false

也就是说,第二个参数 chars 中任意一个字符(Unicode Code Point)如果在第一个参数 s 中存在,则返回 true。

查看这三个函数的源码,发现它们只是调用了相应的 Index 函数(子串出现的位置),然后和 0 作比较返回 true 或 fale。如,Contains:

func Contains(s, substr string) bool {
  return Index(s, substr) >= 0
}

关于 Index 相关函数的实现,我们后面介绍。

2.1.3 子串出现次数 ( 字符串匹配 )

在数据结构与算法中,可能会讲解以下字符串匹配算法:

  • 朴素匹配算法
  • KMP 算法
  • Rabin-Karp 算法
  • Boyer-Moore 算法

还有其他的算法,这里不一一列举,感兴趣的可以网上搜一下。

在 Go 中,查找子串出现次数即字符串模式匹配,实现的是 Rabin-Karp 算法。Count 函数的签名如下:

func Count(s, sep string) int

在 Count 的实现中,处理了几种特殊情况,属于字符匹配预处理的一部分。这里要特别说明一下的是当 sep 为空时,Count 的返回值是:utf8.RuneCountInString(s) + 1

fmt.Println(strings.Count("cheese", "e"))
fmt.Println(len("谷歌中国"))
fmt.Println(strings.Count("谷歌中国", ""))

输出:

3
12  
5  

关于 Rabin-Karp 算法的实现,有兴趣的可以看看 Count 的源码。

另外,Count 是计算子串在字符串中出现的无重叠的次数,比如:

fmt.Println(strings.Count("fivevev", "vev"))

输出:

1

2.1.4 字符串分割为[]string

这个需求很常见,倒不一定是为了得到[]string。

该包提供了六个三组分割函数:Fields 和 FieldsFunc、Split 和 SplitAfter、SplitN 和 SplitAfterN。

2.1.3.1 Fields 和 FieldsFunc

这两个函数的签名如下:

func Fields(s string) []string
func FieldsFunc(s string, f func(rune) bool) []string

Fields 用一个或多个连续的空格分隔字符串 s,返回子字符串的数组(slice)。如果字符串 s 只包含空格,则返回空列表 ([]string 的长度为 0)。其中,空格的定义是 unicode.IsSpace,之前已经介绍过。

常见间隔符包括:’\t’, ‘\n’, ‘\v’, ‘\f’, ‘\r’, ‘ ‘, U+0085 (NEL), U+00A0 (NBSP)

由于是用空格分隔,因此结果中不会含有空格或空子字符串,例如:

fmt.Printf("Fields are: %q", strings.Fields("  foo bar  baz   "))

输出结果:

Fields are: ["foo" "bar" "baz"]

FieldsFunc 用这样的 Unicode 代码点 c 进行分隔:满足 f(c) 返回 true。该函数返回[]string。如果字符串 s 中所有的代码点 (unicode code points) 都满足 f(c) 或者 s 是空,则 FieldsFunc 返回空 slice。

也就是说,我们可以通过实现一个回调函数来指定分隔字符串 s 的字符。比如上面的例子,我们通过 FieldsFunc 来实现:

fmt.Println(strings.FieldsFunc("  foo bar  baz   ", unicode.IsSpace))

实际上,Fields 函数就是调用 FieldsFunc 实现的:

func Fields(s string) []string {
  return FieldsFunc(s, unicode.IsSpace)
}

2.1.3.2 Split 和 SplitAfter、 SplitN 和 SplitAfterN

之所以将这四个函数放在一起讲,是因为它们都是通过一个同一个内部函数来实现的。它们的函数签名及其实现:

func Split(s, sep string) []string { return genSplit(s, sep, 0, -1) }
func SplitAfter(s, sep string) []string { return genSplit(s, sep, len(sep), -1) }
func SplitN(s, sep string, n int) []string { return genSplit(s, sep, 0, n) }
func SplitAfterN(s, sep string, n int) []string { return genSplit(s, sep, len(sep), n) }

它们都调用了 genSplit 函数。

这四个函数都是通过 sep 进行分割,返回[]string。如果 sep 为空,相当于分成一个个的 UTF-8 字符,如 Split("abc",""),得到的是[a b c]。

Split(s, sep) 和 SplitN(s, sep, -1) 等价;SplitAfter(s, sep) 和 SplitAfterN(s, sep, -1) 等价。

那么,Split 和 SplitAfter 有啥区别呢?通过这两句代码的结果就知道它们的区别了:

fmt.Printf("%q\n", strings.Split("foo,bar,baz", ","))
fmt.Printf("%q\n", strings.SplitAfter("foo,bar,baz", ","))

输出:

["foo" "bar" "baz"]
["foo," "bar," "baz"]

也就是说,Split 会将 s 中的 sep 去掉,而 SplitAfter 会保留 sep。

带 N 的方法可以通过最后一个参数 n 控制返回的结果中的 slice 中的元素个数,当 n < 0 时,返回所有的子字符串;当 n == 0 时,返回的结果是 nil;当 n > 0 时,表示返回的 slice 中最多只有 n 个元素,其中,最后一个元素不会分割,比如:

fmt.Printf("%q\n", strings.SplitN("foo,bar,baz", ",", 2))

输出:

["foo" "bar,baz"]

另外看一下官方文档提供的例子,注意一下输出结果:

fmt.Printf("%q\n", strings.Split("a,b,c", ","))
fmt.Printf("%q\n", strings.Split("a man a plan a canal panama", "a "))
fmt.Printf("%q\n", strings.Split(" xyz ", ""))
fmt.Printf("%q\n", strings.Split("", "Bernardo O'Higgins"))

输出:

["a" "b" "c"]
["" "man " "plan " "canal panama"]
[" " "x" "y" "z" " "]
[""]

2.1.4 字符串是否有某个前缀或后缀

这两个函数比较简单,源码如下:

// s 中是否以 prefix 开始
func HasPrefix(s, prefix string) bool {
  return len(s) >= len(prefix) && s[0:len(prefix)] == prefix
}
// s 中是否以 suffix 结尾
func HasSuffix(s, suffix string) bool {
  return len(s) >= len(suffix) && s[len(s)-len(suffix):] == suffix
}

如果 prefix 或 suffix 为 “” , 返回值总是 true。

示例:

fmt.Println(strings.HasPrefix("Gopher", "Go"))
fmt.Println(strings.HasPrefix("Gopher", "C"))
fmt.Println(strings.HasPrefix("Gopher", ""))
fmt.Println(strings.HasSuffix("Amigo", "go"))
fmt.Println(strings.HasSuffix("Amigo", "Ami"))
fmt.Println(strings.HasSuffix("Amigo", ""))

输出结果:

true
false
true
true
false
true

2.1.5 字符或子串在字符串中出现的位置

有一序列函数与该功能有关:

// 在 s 中查找 sep 的第一次出现,返回第一次出现的索引
func Index(s, sep string) int
// 在 s 中查找字节 c 的第一次出现,返回第一次出现的索引
func IndexByte(s string, c byte) int
// chars 中任何一个 Unicode 代码点在 s 中首次出现的位置
func IndexAny(s, chars string) int
// 查找字符 c 在 s 中第一次出现的位置,其中 c 满足 f(c) 返回 true
func IndexFunc(s string, f func(rune) bool) int
// Unicode 代码点 r 在 s 中第一次出现的位置
func IndexRune(s string, r rune) int

// 有三个对应的查找最后一次出现的位置
func LastIndex(s, sep string) int
func LastIndexByte(s string, c byte) int
func LastIndexAny(s, chars string) int
func LastIndexFunc(s string, f func(rune) bool) int

在 2.1.1 小节提到过,Contain 相关的函数内部调用的是响应的 Index 函数。

这一序列函数,只举 IndexFunc 的例子:

han := func(c rune) bool {
    return unicode.Is(unicode.Han, c) // 汉字
}
fmt.Println(strings.IndexFunc("Hello, world", han))
fmt.Println(strings.IndexFunc("Hello, 世界", han))

输出:

-1
7

2.1.6 字符串 JOIN 操作

将字符串数组(或 slice)连接起来可以通过 Join 实现,函数签名如下:

func Join(a []string, sep string) string

假如没有这个库函数,我们自己实现一个,我们会这么实现:

func Join(str []string, sep string) string {
  // 特殊情况应该做处理
  if len(str) == 0 {
      return ""
  }
  if len(str) == 1 {
      return str[0]
  }
  buffer := bytes.NewBufferString(str[0])
  for _, s := range str[1:] {
      buffer.WriteString(sep)
      buffer.WriteString(s)
  }
  return buffer.String()
}

这里,我们使用了 bytes 包的 Buffer 类型,避免大量的字符串连接操作(因为 Go 中字符串是不可变的)。我们再看一下标准库的实现:

func Join(a []string, sep string) string {
  if len(a) == 0 {
      return ""
  }
  if len(a) == 1 {
      return a[0]
  }
  n := len(sep) * (len(a) - 1)
  for i := 0; i < len(a); i++ {
      n += len(a[i])
  }

  b := make([]byte, n)
  bp := copy(b, a[0])
  for _, s := range a[1:] {
      bp += copy(b[bp:], sep)
      bp += copy(b[bp:], s)
  }
  return string(b)
}

标准库的实现没有用 bytes 包,当然也不会简单的通过 + 号连接字符串。Go 中是不允许循环依赖的,标准库中很多时候会出现代码拷贝,而不是引入某个包。这里 Join 的实现方式挺好,我个人猜测,不直接使用 bytes 包,也是不想依赖 bytes 包(其实 bytes 中的实现也是 copy 方式)。

简单使用示例:

fmt.Println(Join([]string{"name=xxx", "age=xx"}, "&"))

输出结果:

name=xxx&age=xx

2.1.7 字符串重复几次

函数签名如下:

func Repeat(s string, count int) string

将 s 重复 count 次,如果 count 为负数或返回值长度 len(s)*count 超出 string 上限会导致 panic,这个函数使用很简单:

fmt.Println("ba" + strings.Repeat("na", 2))

输出结果:

banana

2.1.8 字符替换

func Map(mapping func(rune) rune, s string) string

Map 函数,将 s 的每一个字符按照 mapping 的规则做映射替换,如果 mapping 返回值 <0 ,则舍弃该字符。该方法只能对每一个字符做处理,但处理方式很灵活,可以方便的过滤,筛选汉字等。

示例:

mapping := func(r rune) rune {
    switch {
    case r >= 'A' && r <= 'Z': // 大写字母转小写
        return r + 32
    case r >= 'a' && r <= 'z': // 小写字母不处理
        return r
    case unicode.Is(unicode.Han, r): // 汉字换行
        return '\n'
    }
    return -1 // 过滤所有非字母、汉字的字符
}
fmt.Println(strings.Map(mapping, "Hello你#¥%……\n('World\n,好Hello^(&(*界gopher..."))

输出结果:

hello
world
hello
gopher

2.1.9 字符串子串替换

进行字符串替换时,考虑到性能问题,能不用正则尽量别用,应该用这里的函数。

字符串替换的函数签名如下:

// 用 new 替换 s 中的 old,一共替换 n 个。
// 如果 n < 0,则不限制替换次数,即全部替换
func Replace(s, old, new string, n int) string
// 该函数内部直接调用了函数 Replace(s, old, new , -1)
func ReplaceAll(s, old, new string) string

使用示例:

fmt.Println(strings.Replace("oink oink oink", "k", "ky", 2))
fmt.Println(strings.Replace("oink oink oink", "oink", "moo", -1))
fmt.Println(strings.ReplaceAll("oink oink oink", "oink", "moo"))

输出:

oinky oinky oink
moo moo moo
moo moo moo

如果我们希望一次替换多个,比如我们希望替换 This is <b>HTML</b> 中的 <>&lt;&gt;,可以调用上面的函数两次。但标准库提供了另外的方法进行这种替换。

2.1.10 大小写转换

func ToLower(s string) string
func ToLowerSpecial(c unicode.SpecialCase, s string) string
func ToUpper(s string) string
func ToUpperSpecial(c unicode.SpecialCase, s string) string

大小写转换包含了 4 个相关函数,ToLower,ToUpper 用于大小写转换。ToLowerSpecial,ToUpperSpecial 可以转换特殊字符的大小写。
举个例子:

fmt.Println(strings.ToLower("HELLO WORLD"))
fmt.Println(strings.ToLower("Ā Á Ǎ À"))
fmt.Println(strings.ToLowerSpecial(unicode.TurkishCase, "壹"))
fmt.Println(strings.ToLowerSpecial(unicode.TurkishCase, "HELLO WORLD"))
fmt.Println(strings.ToLower("Önnek İş"))
fmt.Println(strings.ToLowerSpecial(unicode.TurkishCase, "Önnek İş"))

fmt.Println(strings.ToUpper("hello world"))
fmt.Println(strings.ToUpper("ā á ǎ à"))
fmt.Println(strings.ToUpperSpecial(unicode.TurkishCase, "一"))
fmt.Println(strings.ToUpperSpecial(unicode.TurkishCase, "hello world"))
fmt.Println(strings.ToUpper("örnek iş"))
fmt.Println(strings.ToUpperSpecial(unicode.TurkishCase, "örnek iş"))

输出结果:

hello world
ā á ǎ à
壹
hello world
önnek iş
önnek iş
HELLO WORLD
Ā Á Ǎ À       // 汉字拼音有效
一           //  汉字无效
HELLO WORLD
ÖRNEK IŞ
ÖRNEK İŞ    // 有细微差别

2.1.10 标题处理

func Title(s string) string
func ToTitle(s string) string
func ToTitleSpecial(c unicode.SpecialCase, s string) string

标题处理包含 3 个相关函数,其中 Title 会将 s 每个单词的首字母大写,不处理该单词的后续字符。ToTitle 将 s 的每个字母大写。ToTitleSpecial 将 s 的每个字母大写,并且会将一些特殊字母转换为其对应的特殊大写字母。

举个例子:

fmt.Println(strings.Title("hElLo wOrLd"))
fmt.Println(strings.ToTitle("hElLo wOrLd"))
fmt.Println(strings.ToTitleSpecial(unicode.TurkishCase, "hElLo wOrLd"))
fmt.Println(strings.Title("āáǎà ōóǒò êēéěè"))
fmt.Println(strings.ToTitle("āáǎà ōóǒò êēéěè"))
fmt.Println(strings.ToTitleSpecial(unicode.TurkishCase, "āáǎà ōóǒò êēéěè"))
fmt.Println(strings.Title("dünyanın ilk borsa yapısı Aizonai kabul edilir"))
fmt.Println(strings.ToTitle("dünyanın ilk borsa yapısı Aizonai kabul edilir"))
fmt.Println(strings.ToTitleSpecial(unicode.TurkishCase, "dünyanın ilk borsa yapısı Aizonai kabul edilir"))

输出结果:

HElLo WOrLd
HELLO WORLD
HELLO WORLD
Āáǎà Ōóǒò Êēéěè
ĀÁǍÀ ŌÓǑÒ ÊĒÉĚÈ
ĀÁǍÀ ŌÓǑÒ ÊĒÉĚÈ
Dünyanın Ilk Borsa Yapısı Aizonai Kabul Edilir
DÜNYANIN ILK BORSA YAPISI AIZONAI KABUL EDILIR
DÜNYANIN İLK BORSA YAPISI AİZONAİ KABUL EDİLİR

2.1.11 修剪

// 将 s 左侧和右侧中匹配 cutset 中的任一字符的字符去掉
func Trim(s string, cutset string) string
// 将 s 左侧的匹配 cutset 中的任一字符的字符去掉
func TrimLeft(s string, cutset string) string
// 将 s 右侧的匹配 cutset 中的任一字符的字符去掉
func TrimRight(s string, cutset string) string
// 如果 s 的前缀为 prefix 则返回去掉前缀后的 string , 否则 s 没有变化。
func TrimPrefix(s, prefix string) string
// 如果 s 的后缀为 suffix 则返回去掉后缀后的 string , 否则 s 没有变化。
func TrimSuffix(s, suffix string) string
// 将 s 左侧和右侧的间隔符去掉。常见间隔符包括:'\t', '\n', '\v', '\f', '\r', ' ', U+0085 (NEL)
func TrimSpace(s string) string
// 将 s 左侧和右侧的匹配 f 的字符去掉
func TrimFunc(s string, f func(rune) bool) string
// 将 s 左侧的匹配 f 的字符去掉
func TrimLeftFunc(s string, f func(rune) bool) string
// 将 s 右侧的匹配 f 的字符去掉
func TrimRightFunc(s string, f func(rune) bool) string

包含了 9 个相关函数用于修剪字符串。

举个例子:

x := "!!!@@@你好,!@#$ Gophers###$$$"
fmt.Println(strings.Trim(x, "@#$!%^&*()_+=-"))
fmt.Println(strings.TrimLeft(x, "@#$!%^&*()_+=-"))
fmt.Println(strings.TrimRight(x, "@#$!%^&*()_+=-"))
fmt.Println(strings.TrimSpace(" \t\n Hello, Gophers \n\t\r\n"))
fmt.Println(strings.TrimPrefix(x, "!"))
fmt.Println(strings.TrimSuffix(x, "$"))

f := func(r rune) bool {
    return !unicode.Is(unicode.Han, r) // 非汉字返回 true
}
fmt.Println(strings.TrimFunc(x, f))
fmt.Println(strings.TrimLeftFunc(x, f))
fmt.Println(strings.TrimRightFunc(x, f))

输出结果:

你好,!@#$ Gophers
你好,!@#$ Gophers###$$$
!!!@@@你好,!@#$ Gophers
Hello, Gophers
!!@@@你好,!@#$ Gophers###$$$
!!!@@@你好,!@#$ Gophers###$$
你好
你好,!@#$ Gophers###$$$
!!!@@@你好

2.1.12 Replacer 类型

这是一个结构,没有导出任何字段,实例化通过 func NewReplacer(oldnew ...string) *Replacer 函数进行,其中不定参数 oldnew 是 old-new 对,即进行多个替换。如果 oldnew 长度与奇数,会导致 panic.

示例:

r := strings.NewReplacer("<", "&lt;", ">", "&gt;")
fmt.Println(r.Replace("This is <b>HTML</b>!"))

输出结果:

This is &lt;b&gt;HTML&lt;/b&gt;!

另外,Replacer 还提供了另外一个方法,它在替换之后将结果写入 io.Writer 中。

func (r *Replacer) WriteString(w io.Writer, s string) (n int, err error)

2.1.13 Reader 类型

看到名字就能猜到,这是实现了 io 包中的接口。它实现了 io.Reader(Read 方法),io.ReaderAt(ReadAt 方法),io.Seeker(Seek 方法),io.WriterTo(WriteTo 方法),io.ByteReader(ReadByte 方法),io.ByteScanner(ReadByte 和 UnreadByte 方法),io.RuneReader(ReadRune 方法) 和 io.RuneScanner(ReadRune 和 UnreadRune 方法)。

Reader 结构如下:

type Reader struct {
  s        string    // Reader 读取的数据来源
  i        int // current reading index(当前读的索引位置)
  prevRune int // index of previous rune; or < 0(前一个读取的 rune 索引位置)
}

可见 Reader 结构没有导出任何字段,而是提供一个实例化方法:

func NewReader(s string) *Reader

该方法接收一个字符串,返回的 Reader 实例就是从该参数字符串读数据。在后面学习了 bytes 包之后,可以知道 bytes.NewBufferString 有类似的功能,不过,如果只是为了读取,NewReader 会更高效。

其他方法不介绍了,都是之前接口的实现,有兴趣的可以看看源码实现,大部分都是根据 i、prevRune 两个属性来控制。

2.1.14 Builder 类型

type Builder struct {
    addr *Builder // of receiver, to detect copies by value
    buf  []byte
}

该类型实现了 io 包下的 Writer, ByteWriter, StringWriter 等接口,可以向该对象内写入数据,Builder 没有实现 Reader 等接口,所以该类型不可读,但提供了 String 方法可以获取对象内的数据。

// 该方法向 b 写入一个字节
func (b *Builder) WriteByte(c byte) error
// WriteRune 方法向 b 写入一个字符
func (b *Builder) WriteRune(r rune) (int, error)
// WriteRune 方法向 b 写入字节数组 p
func (b *Builder) Write(p []byte) (int, error)
// WriteRune 方法向 b 写入字符串 s
func (b *Builder) WriteString(s string) (int, error)
// Len 方法返回 b 的数据长度。
func (b *Builder) Len() int
// Cap 方法返回 b 的 cap。
func (b *Builder) Cap() int
// Grow 方法将 b 的 cap 至少增加 n (可能会更多)。如果 n 为负数,会导致 panic。
func (b *Builder) Grow(n int)
// Reset 方法将 b 清空 b 的所有内容。
func (b *Builder) Reset()
// String 方法将 b 的数据以 string 类型返回。
func (b *Builder) String() string

Builder 有 4 个与写入相关的方法,这 4 个方法的 error 都总是为 nil.

Builder 的 cap 会自动增长,一般不需要手动调用 Grow 方法。

String 方法可以方便的获取 Builder 的内容。

举个例子:

b := strings.Builder{}
_ = b.WriteByte('7')
n, _ := b.WriteRune('夕')
fmt.Println(n)
n, _ = b.Write([]byte("Hello, World"))
fmt.Println(n)
n, _ = b.WriteString("你好,世界")
fmt.Println(n)
fmt.Println(b.Len())
fmt.Println(b.Cap())
b.Grow(100)
fmt.Println(b.Len())
fmt.Println(b.Cap())
fmt.Println(b.String())
b.Reset()
fmt.Println(b.String())

输出结果:

3
12
15
31
32
31
164
7夕Hello, World你好,世界

原文链接:https://books.studygolang.com/The-Golang-Standard-Library-by-Example/chapter02/02.1.html#