Go语言中的字符串操作
洪笳淏 Lv4

注:本节内容参考自《Go语言标准库》,结合自己实际的使用体验进行总结。

字符串的常见操作

  • 字符串长度
  • 求子串
  • 是否存在某个字符或子串
  • 子串出现的次数(字符串匹配)
  • 字符串切割为[]string
  • 字符串是否存在某个前缀或后缀
  • 字符或子串在字符串中首次出现的位置或最后一次出现的位置
  • 通过某个字符串将[]string连接起来
  • 字符串重复几次
  • 字符串中子串替换
  • 大小写转换
  • Trim操作

  string类型可以看做一种特殊的slice类型,因此获取长度可以使用内置的函数 len(),同时支持切片操作,因此子串很容易获得。这里说的字符,指得是 rune 类型,即一个 UTF-8 字符(Unicode 代码点)

1.字符串比较

1
2
3
4
5
// Compare函数,用于比较两个字符串的大小,如果两个字符串相等,返回0;如果a小于b,返回-1;如果a大于b,返回1
func Compare(a,b string) int

// EqualFold函数,计算s与t忽略字母大小写是否相等
func EqualFold(s,t string) bool

示例:

1
2
3
4
5
6
7
8
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
2
3
4
5
-1
0
1
true
false

2.是否存在某个字符或子串

1
2
3
4
5
6
// 子串 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 函数进行一下说明,看如下例子:

1
2
3
4
5
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("", ""))

输出:

1
2
3
4
5
false
true
true
false
false

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

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

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

3.子串出现的次数

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

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

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

1
func Count(s, sep string) int

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

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

输出:

1
2
3
3
12
5

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

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

输出:

1
1

4.字符串分割为[]string

Fields 和 FieldsFunc

这两个函数的签名如下:

1
2
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)。

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

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

输出结果:

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

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

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

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

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

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

题目示例:

2062. 统计字符串中的元音子字符串

子字符串 是字符串中的一个连续(非空)的字符序列。

元音子字符串 由元音('a''e''i''o''u')组成的一个子字符串,且必须包含 全部五种 元音。给你一个字符串 word ,统计并返回 word元音子字符串的数目

示例 1:

1
2
3
4
5
输入:word = "aeiouu"
输出:2
解释:下面列出 word 中的元音子字符串(斜体加粗部分):
- "aeiouu"
- "aeiouu"

示例 2:

1
2
3
输入:word = "unicornarihan"
输出:0
解释:word 中不含 5 种元音,所以也不会存在元音子字符串。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func countVowelSubstrings(word string) (ans int) {
for _, s := range strings.FieldsFunc(word, func(r rune) bool { return !strings.ContainsRune("aeiou", r) }) { // 分割出仅包含元音的字符串

cnt := ['v']int{}
l := 0
for _, ch := range s {
cnt[ch]++
for cnt[s[l]] > 1 { // 双指针,仅当该元音个数不止一个时才移动左指针
cnt[s[l]]--
l++
}
if cnt['a'] > 0 && cnt['e'] > 0 && cnt['i'] > 0 && cnt['o'] > 0 && cnt['u'] > 0 { // 必须包含全部五种元音
ans += l + 1
}
}
}
return
}

Split、SplitAfter、SplitN和SplitAfterN

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

1
2
3
4
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 有啥区别呢?通过这两句代码的结果就知道它们的区别了:

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

输出:

1
2
["foo" "bar" "baz"]
["foo," "bar," "baz"]

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

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

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

输出:

1
["foo" "bar,baz"]

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

1
2
3
4
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"))

输出:

1
2
3
4
["a" "b" "c"]
["" "man " "plan " "canal panama"]
[" " "x" "y" "z" " "]
[""]

5.字符串中是否有某个前缀或后缀

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

1
2
3
4
5
6
7
8
// 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。

示例:

1
2
3
4
5
6
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", ""))

输出结果:

1
2
3
4
5
6
true
false
true
true
false
true

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 在 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 的例子:

1
2
3
4
5
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
2
-1
7

7.字符串拼接(JOIN)操作

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

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

看一下标准库的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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)
}

简单使用示例:

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

输出结果:

1
name=xxx&age=xx

8.字符串重复几次

函数签名如下:

1
func Repeat(s string, count int) string

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

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

输出结果:

1
banana

9.字符替换

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

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

示例:

1
2
3
4
5
6
7
8
9
10
11
12
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..."))

输出结果:

1
2
3
4
hello
world
hello
gopher

10.字符串子串替换

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

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

1
2
3
4
5
// 用 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

使用示例:

1
2
3
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"))

输出:

1
2
3
oinky oinky oink
moo moo moo
moo moo moo

11.大小写转换

1
2
3
4
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 可以转换特殊字符的大小写。 举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
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ş"))

输出结果:

1
2
3
4
5
6
7
8
9
10
11
12
hello world
ā á ǎ à

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

12.标题处理

1
2
3
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 的每个字母大写,并且会将一些特殊字母转换为其对应的特殊大写字母。

举个例子:

1
2
3
4
5
6
7
8
9
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"))

输出结果:

1
2
3
4
5
6
7
8
9
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

13.修剪(Trim)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 将 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 个相关函数用于修剪字符串。

举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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))

输出结果:

1
2
3
4
5
6
7
8
9
你好,!@#$ Gophers
你好,!@#$ Gophers###$$$
!!!@@@你好,!@#$ Gophers
Hello, Gophers
!!@@@你好,!@#$ Gophers###$$$
!!!@@@你好,!@#$ Gophers###$$
你好
你好,!@#$ Gophers###$$$
!!!@@@你好

14.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 结构如下:

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

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

1
func NewReader(s string) *Reader

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

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

15.Builder类型

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 该方法向 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 的内容。

举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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())

输出结果:

1
2
3
4
5
6
7
8
3
12
15
31
32
31
164
7夕Hello, World你好,世界
  • Post title:Go语言中的字符串操作
  • Post author:洪笳淏
  • Create time:2021-11-10 19:30:00
  • Post link:https://jiahaohong1997.github.io/2021/11/10/Go语言中的字符串操作/
  • Copyright Notice:All articles in this blog are licensed under BY-NC-SA unless stating additionally.
 Comments