Go语言中关于字符串的处理库 strings

871 阅读10分钟

Go语言有丰富的内置库和优秀的第三方库,无论是在日常的开发工作中,还是基于内置库进行开源项目开发,内置库的使用都是必不可少的,我们使用内置库也很频繁,并且无数优秀的第三方库也都是根据内置库不断扩充和完善的,以降低开发者使用难度。

我们也可以经常查看内置库的源代码,关注其用法和命名方式等,对持续提高个人开发水平和能力也是大有裨益。

字符串(string)在日常开发过程中使用频繁的数据类型之一。strings 包实现了操作 UTF-8 编码字符串的简单的函数。大家可能在实际开发过程中处理过各种关于字符串的需求,而常见的关于字符串有哪些呢?

  • 某个字符串中是否包含另一个字符串
  • 获取字符串某个子串的第一字符的位置
  • 去除字符串前后指定的字符
  • 将字符串以某个特定的字符分割成字符串数组
  • 统计某字符出现的次数
  • 大小写转换
  • 判断某个字符串是否以另一个字符串开头或者结尾

我们以几种常见的操作来简单阐述一下 strings 包的使用,当然,如果可以的话,我更推荐你去看下源码实现或者官方文档,官方文档地址:pkg.go.dev/strings。没有什么能比官方提供的文档说明更清晰的了。

某个字符串中是否包含另一个字符串

在 strings 包里,有三个函数类似的函数,分别如下:

func Contains(s, substr string) bool
func ContainsAny(s, chars string) bool
func ContainsRune(s string, r rune) bool

三个函数虽然都可以判断字符串是否包含另一个子串,但是用法却不尽相同。strings.Contains 是字符串 s 中是否包含 substr。strings.ContainsAny 是表示子串 chars 任一字符在字符串 s 中出现即可。而strings.ContainsRune 中我们可以看到 r 的数据类型是 rune,表示字符在 Unicode 中的码点字面量(Unicode code point)。 我们可以看几个示例:

package main

import (
	"fmt"
	"strings"
)

func main() {
    // Contains
	fmt.Println(strings.Contains("seafood", "foo"))  // true
	fmt.Println(strings.Contains("seafood", "bar"))  // false
	fmt.Println(strings.Contains("seafood", ""))     // true
	fmt.Println(strings.Contains("", ""))			 // true
    
    // ContainsAny 
    fmt.Println(strings.ContainsAny("team", "i"))     // false
	fmt.Println(strings.ContainsAny("fail", "ui"))    // true
	fmt.Println(strings.ContainsAny("ure", "ui"))     // true
	fmt.Println(strings.ContainsAny("failure", "ui")) // true
	fmt.Println(strings.ContainsAny("foo", ""))       // false
	fmt.Println(strings.ContainsAny("", ""))          // false
    
    // ContainsRune ,注意,小字字母 a 的Unicode码点字面量是97.
    fmt.Println(strings.ContainsRune("aardvark", 97)) // true
	fmt.Println(strings.ContainsRune("timeout", 97))  // false
}

字符串比较

在 Go 语言中,关于字符串的比较关系,可以通过非负值表示大于关系,负值表示小于关系,零值表示相等的关系。

package main

import (
	"fmt"
	"strings"
)

func main() {
	fmt.Println(strings.Compare("a", "b"))                       // -1
	fmt.Println(strings.Compare("a", "a"))                       // 0
	fmt.Println(strings.Compare("b", "a"))                       // 1
	fmt.Println(strings.Compare("a", "b"), rune('a'), rune('b')) // -1 97 98
}

字符串的比较方法是根据单个字符的ASCII编码来进行的,比如a的编码为97,b的编码是98,所以 a 与 b 进行比较,就会返回 -1。

大小写转换

字母大小写转换是比较常用的字符串操作了,而在 strings 包中,提供了如下函数:

func ToUpper(s string) string
func ToLower(s string) string
func ToTitle(s string) string
func Title(s string) string

其中从命名上我们也能大概猜出来各函数的具体作用,废话不多说,看示例你会更加明白:

package main

import (
	"fmt"
	"strings"
)

func main() {

	var b string = "hello wORld! 你好中国!"
    fmt.Println("ToUpper:", strings.ToUpper(b))
	fmt.Println("ToLower:", strings.ToLower(b))
	fmt.Println("ToTitle:", strings.ToTitle(b))
	fmt.Println("Title:", strings.Title(b))
}

// OutPut:
/*
ToUpper: HELLO WORLD! 你好中国!
ToLower: hello world! 你好中国!
ToTitle: HELLO WORLD! 你好中国!
Title: Hello WORld! 你好中国!
*/

看到结果,大家也许会有疑问, ToUpper 和 ToTitle 的结果是一样,那这两个有什么区别呢?这两个函数非常类似,都是将所有字符变成大写,他们区别特别微小,主要在于 Unicode 规定的区别。 我们再看个示例:

package main

import (
	"fmt"
	"strings"
)

func main() {
    str := "dz"
    tt := strings
    
	tt := strings.ToTitle(str)
	tu := strings.ToUpper(str)

	ttrune, _ := utf8.DecodeRuneInString(tt)
	turune, _ := utf8.DecodeRuneInString(tu)
	fmt.Println("ToTitle result:", tt, "\t", ttrune)
	fmt.Println("ToUpper result:", tu, "\t", turune)
}

// OutPut:
/*
ToTitle result: Dz 	 498
ToUpper result: DZ 	 497
*/

由此可见 ToTitle 与 ToUpper 的细微区别,关于这两者的区别,在 Stackoverflow 上有也相关的讨论,地址如下: stackoverflow.com/questions/1… 。你可以具体的看一下。 上面的代码需要注意一下,dz 是一个字符,而是由d和z组合成的字符。

另外一个函数 Title 的作用是将每个单词的首字母变成大写。但是其他字母的大小写不会转换。不过我们需要注意一下,该函数在 Go1.18 版本已经被废弃,被标识为 DEPRECATED,在文档中也是默认被折叠的。 image.png 从上图中,也可以看到,我们可以使用 golang.org/x/text/cases 来代替 strings.Title,我们看一个示例:

package main

import (
	"fmt"
	"golang.org/x/text/cases"
	"golang.org/x/text/language"
)

func main() {
    var b string = "hello wORld! 你好中国!"
    caser := cases.Title(language.English)
	fmt.Println(caser.String(b))
}
// OutPut:
// Hello World! 你好中国!

注意一下,cases.Title 返回的类型是 Caser,它可以接受 string 类型或者 bytes 类型,我们看到上面的结果是将 每个单词的首字母变成大写,而其他字母则变成了小写,其实与 strings.Title 也是有一定的区别的。

统计某字符串出现的次数

// Count counts the number of non-overlapping instances of substr in s.
// If substr is an empty string, Count returns 1 + the number of Unicode code points in s.
func Count(s, substr string) int

Count 计算 s 中 substr 的非重叠实例的数量。 如果 substr 是空字符串,则 Count 返回 1 + s 中的 Unicode 代码点数。一起看个示例:

package main

import (
	"fmt"
	"strings"
)

func main() {
	fmt.Println(strings.Count("cheese", "e"))
	fmt.Println(strings.Count("你好Cheese", "")) // before & after each rune
}
// Output:
// 3
// 9

一个 string 类型的值会由若干个 Unicode 字符组成,每个 Unicode 字符都可以由一个 rune 类型的值来承载。所以上面示例第二条返回的值是9。

判断字符串的前后缀

进行字符串前后缀验证就是判断字符串截取之后的子字符串是否相等,所以其源码比较简单:

// HasPrefix tests whether the string s begins with prefix.
func HasPrefix(s, prefix string) bool {
	return len(s) >= len(prefix) && s[0:len(prefix)] == prefix
}

// HasSuffix tests whether the string s ends with suffix.
func HasSuffix(s, suffix string) bool {
	return len(s) >= len(suffix) && s[len(s)-len(suffix):] == suffix
}

我们需要注意的,如果 prefix 或者 suffix 为 "" 的话,都会返回 true。

索引

在 strings 包中涉及到索引的函数比较多一些,函数如下:

// Index returns the index of the first instance of substr in s, or -1 if substr is not present in s.
func Index(s, substr string) int

// IndexAny returns the index of the first instance of any Unicode code point
// from chars in s, or -1 if no Unicode code point from chars is present in s.
func IndexAny(s, chars string) int

// IndexByte returns the index of the first instance of c in s, or -1 if c is not present in s.
func IndexByte(s string, c byte) int

// IndexRune returns the index of the first instance of the Unicode code point
// r, or -1 if rune is not present in s.
// If r is utf8.RuneError, it returns the first instance of any
// invalid UTF-8 byte sequence.
func IndexRune(s string, r rune) int

// IndexFunc returns the index into s of the first Unicode
// code point satisfying f(c), or -1 if none do.
func IndexFunc(s string, f func(rune) bool) int

以上的函数,都会返回查找字符的索引位置,如果原字符串有相同的字符,则会返回从左至右查到的第一个的索引。其中 Index 函数也是使用最频繁的函数之一,无论是在日常开发中,还是在Go源码当中,都是不可或缺的。

当然,strings 包还提供了一系列的 LastIndex 类的函数,也是查找索引位置,但是查找的是从右至左查到的第一个索引,大家也可以注意一下。

字符串分割

我们经常会遇到将字符串通过某个指定的分割符进行分割,生成一个字符串数组。比如Java中的 split 函数,Javascript 中的 split 函数,Python 中的 split 函数。在Go 语言中,也是叫 Split 。我们看下示例:

package main

import (
	"fmt"
	"strings"
)

func main() {
	fmt.Printf("%q\n", strings.Split("apple,orange,banana,peach,strawberry", ","))
	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"))
}
// Output
// ["apple" "orange" "banana" "peach" "strawberry"]
// ["" "man " "plan " "canal panama"]
// [" " "x" "y" "z" " "]
// [""]

可以看出来,如果是以 "" 为分割符的话,意味着将每一个字符进行切割;而如果将一个 "" 以任意分割符分割的话,只会返回一个空字符串数组。

当然,合久必分,分久必合,strings 包中同样存在一个与Split相反的函数 Join,一个按照指定的字符数组连接成字符串,Join 函数如下:

// Join concatenates the elements of its first argument to create a single string. The separator
// string sep is placed between elements in the resulting string.
func Join(elems []string, sep string) string

这里就不详细赘述该函数的使用了。

去除字符串前后指定的字符

如果你正好有去除字符串前后指定的字符的需求的话,在 strings 包中提供了一系列相关的函数:

func Trim(s, cutset string) string
func TrimSpace(s string) string
func TrimLeft(s, cutset string) string
func TrimRight(s, cutset string) string
func TrimPrefix(s, prefix string) string
func TrimSuffix(s, suffix string) string

如果我们需要把一个字符串两端的空格去除掉,那我们我们可以使用 Trim 函数,但是除此之外,strings 包还专门提供了一个 TrimSpace 函数:

package main

import (
	"fmt"
	"strings"
)

func main() {
	fmt.Println(strings.Trim("   Hello, little girl  ", " "))
	fmt.Println(strings.TrimSpace("   Hello, little girl  "))
}
// Output
// Hello, little girl
// Hello, little girl

我们在使用 Trim 的时候,需要注意一下,Trim 会删除字符串前后两端所有包含要去除的Unicode代码点,看下示例:

package main

import (
	"fmt"
	"strings"
)

func main() {
	fmt.Println(strings.Trim("¡¡¡Hello, Gophers!!!", "!¡"))
	fmt.Println(strings.Trim("abcefpng.png", "abgnp"))
}
// Output
// Hello, Gophers
// cefpng.

由此,可以看出,使用 Trim 的时候,无论要去除的字符顺序如何,只要在前后包含,就会去除。同样,strings 包还提供了 TrimLeft 和 TrimRight 两个函数,其他作用与 Trim 类似,但是只是适用于左侧或者右侧。

而提供的 TrimPrefix 和 TrimSuffix,则需要全部匹配才可以去除。比如,我们只想获取图像的名称,需要把图片的后缀 .png 去掉,就可以使用 TrimSuffix 函数:

package main

import (
	"fmt"
	"strings"
)

func main() {
	fmt.Println(strings.TrimSuffix("abcef.png", ".png"))
}
// Output
// abcef

除此之外,还提供了其他灵活的函数,比如 TrimFunc/TrimLeftFunc/TrimRightFunc 等函数,可以参考源码或者官方文档。

替换操作

字符串替换的操作我们经常遇见,在 strings 包里,提供了两个函数,Replace 和 ReplaceAll。

func Replace(s, old, new string, n int) string
func ReplaceAll(s, old, new string) string

Replace 有四个参数,分别是源字符串、要替换的字符串、被替换的字符串以及要替换的数量。其中如果 n < 0,则表示全部替换。而ReplaceAll 则表示全部替换。

package main

import (
	"fmt"
	"strings"
)

func main() {
	fmt.Println(strings.Replace("mnop,mnop,mnop", "mn", "vp", 1))
    fmt.Println(strings.ReplaceAll("mnop,mnop,mnop", "mn", "vp"))
}
// Output:
// vpop,mnop,mnop
// vpop,vpop,vpop

函数 ReplaceAll 与 函数 Replace 的第四个参数 n < 0 时的作用一致,我们看下 ReplaceAll 的源码其实就能明白:

// ReplaceAll returns a copy of the string s with all
// non-overlapping instances of old replaced by new.
// If old is empty, it matches at the beginning of the string
// and after each UTF-8 sequence, yielding up to k+1 replacements
// for a k-rune string.
func ReplaceAll(s, old, new string) string {
	return Replace(s, old, new, -1)
}

如果我们只是常规性的指定某一类替换操作,但是如果我们需要对整个字符串的多个不同的字符进行替换,如果依旧用Replace函数的话,不仅麻烦而且效率极低,那有什么办法能够既简单又高效呢? 在 strings 包中提供了 Replacer 类型的方法,它将字符串列表替换为replaces,并且多个goroutine同时使用它是安全的。我们看一个官方提示的示例:

package main

import (
	"fmt"
	"strings"
)

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

我们可以看下 strings.NewReplacer() 函数的参数:

// NewReplacer returns a new Replacer from a list of old, new string
// pairs. Replacements are performed in the order they appear in the
// target string, without overlapping matches. The old string
// comparisons are done in argument order.
//
// NewReplacer panics if given an odd number of arguments.
func NewReplacer(oldnew ...string) *Replacer

NewReplacer 会接收多对新旧字符串,并返回Replacer类型,替换是按照它们在目标字符串中出现的顺序执行的,没有重叠的匹配。因为参数是成对出现的,如果出现奇数个参数,会出现panic。而 Replacer 类型源码如下:

// Replacer replaces a list of strings with replacements.
// It is safe for concurrent use by multiple goroutines.
type Replacer struct {
	once   sync.Once // guards buildOnce method
	r      replacer
	oldnew []string
}

// replacer is the interface that a replacement algorithm needs to implement.
type replacer interface {
	Replace(s string) string
	WriteString(w io.Writer, s string) (n int, err error)
}

总结

在 Go 语言中,字符串(strings)是不可变的,并且 strings 包提供了一系列针对字符串的函数,在日常开发过程中也是经常能用到的。