字符串常见操作有:
- 字符串长度;
- 求子串;
- 是否存在某个字符或子串;
- 子串出现的次数(字符串匹配);
- 字符串分割(切分)为[]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("壹", "一"))
输出结果:
-101truefalse
2.1.2 是否存在某个字符或子串
有三个函数做这件事:
// 子串 substr 在 s 中,返回 truefunc Contains(s, substr string) bool// chars 中任何一个 Unicode 代码点在 s 中,返回 truefunc ContainsAny(s, chars string) bool// Unicode 代码点 r 在 s 中,返回 truefunc 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("", ""))
输出:
falsetruetruefalsefalse
也就是说,第二个参数 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("谷歌中国", ""))
输出:
3125
关于 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) []stringfunc 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 函数。
Split("abc","")
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", ""))
输出结果:
truefalsetruetruefalsetrue
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) 返回 truefunc IndexFunc(s string, f func(rune) bool) int// Unicode 代码点 r 在 s 中第一次出现的位置func IndexRune(s string, r rune) int// 有三个对应的查找最后一次出现的位置func LastIndex(s, sep string) intfunc LastIndexByte(s string, c byte) intfunc LastIndexAny(s, chars string) intfunc 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))
输出:
-17
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 + 32case r >= 'a' && r <= 'z': // 小写字母不处理return rcase unicode.Is(unicode.Han, r): // 汉字换行return '\n'}return -1 // 过滤所有非字母、汉字的字符}fmt.Println(strings.Map(mapping, "Hello你#¥%……\n('World\n,好Hello^(&(*界gopher..."))
输出结果:
helloworldhellogopher
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 oinkmoo moo moomoo moo moo
This is HTML<><>
2.1.10 大小写转换
func ToLower(s string) stringfunc ToLowerSpecial(c unicode.SpecialCase, s string) stringfunc ToUpper(s string) stringfunc 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) stringfunc ToTitle(s string) stringfunc 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 WOrLdHELLO WORLDHELLO WORLDĀáǎà Ōóǒò ÊēéěèĀÁǍÀ ŌÓǑÒ ÊĒÉĚÈĀÁǍÀ ŌÓǑÒ ÊĒÉĚÈDünyanın Ilk Borsa Yapısı Aizonai Kabul EdilirDÜNYANIN ILK BORSA YAPISI AIZONAI KABUL EDILIRDÜ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###$$$!!!@@@你好,!@#$ GophersHello, Gophers!!@@@你好,!@#$ Gophers###$$$!!!@@@你好,!@#$ Gophers###$$你好你好,!@#$ Gophers###$$$!!!@@@你好
2.1.12 Replacer 类型
func NewReplacer(oldnew ...string) *Replacer
示例:
r := strings.NewReplacer("<", "<", ">", ">")fmt.Println(r.Replace("This is <b>HTML</b>!"))
输出结果:
This is <b>HTML</b>!
另外,Replacer 还提供了另外一个方法,它在替换之后将结果写入 io.Writer 中。
func (r *Replacer) WriteString(w io.Writer, s string) (n int, err error)
2.1.13 Reader 类型
io
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 valuebuf []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 写入字节数组 pfunc (b *Builder) Write(p []byte) (int, error)// WriteRune 方法向 b 写入字符串 sfunc (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())
输出结果:
导航
312153132311647夕Hello, World你好,世界