Go 字符串及strings包常见操作

1、字符串的定义

字符串是不可变值类型,内部用指针指向 UTF-8 字节数组。

Go 语言中可以使用反引号或者双引号来定义字符串。反引号表示原生的字符串,即不进行转义。Go 语言的字符串不支持单引号

• 默认值是空字符串 ""。
• 用索引号访问某字节,如 s[i]。
• 不能用序号获取字节元素指针,&s[i] 非法。 
• 不可变类型,无法修改字节数组。
• 字节数组尾部不包含 NULL。
  • 双引号

字符串使用双引号括起来,其中的相关的转义字符将被替换。例如:

str := "Hello World! \n Hello Gopher! \n"

输出:
Hello World! 
Hello Gopher!
  • 反引号

字符串使用反引号括起来,其中的相关的转义字符不会被替换。例如:

str :=  `Hello World! \n Hello Gopher! \n` 

输出:
Hello World! \nHello Gopher! \n

总结:双引号中的转义字符被替换,而反引号中原生字符串中的 \n 会被原样输出。

2、字符串转义符

Go 语言的字符串常见转义符包含回车、换行、单双引号、制表符等,如下表所示。

转义含义
\r回车符(返回行首)
\n换行符(直接跳到下一行的同列位置)
\t制表符
单引号
"双引号
\\反斜杠自身

举个例子:

fmt.Println("人生苦短\n我要转go")

3、多行字符串

Go语言中要定义一个多行字符串时,就必须使用反引号字符

s1 := `第一行
    第二行
    第三行
    `
    fmt.Println(s1)

反引号间换行将被作为字符串中的换行,但是所有的转义字符均无效,文本将会原样输出。

4、byte 和 rune 类型

单引号 可 直接 打印 字符 对应的 ASCII码值

组成每个字符串的元素叫做“字符”,可以通过遍历或者单个获取字符串元素获得字符。 字符用单引号(’)包裹起来,如:

var s1 := 'a'       // 97

var s2 := 'A'       // 65

Go 语言的字符有以下两种:

  • uint8类型,或者叫 byte 型,代表了ASCII码的一个字符
  • rune类型,代表一个 UTF-8字符

字符串底层是一个byte数组,所以可以和[]byte类型相互转换。字符串是不能修改的 字符串是由byte字节组成,所以字符串的长度是byte字节的长度。 rune类型用来表示utf8字符,一个rune字符由一个或多个byte组成。

当需要处理中文、日文或者其他复合字符时,则需要用到rune类型。rune类型实际是一个int32。 Go 使用了特殊的 rune 类型来处理 Unicode,让基于 Unicode的文本处理更为方便,也可以使用 byte 型进行默认字符串处理,性能和扩展性都有照顾。

实际上,Go语言的range循环在处理字符串的时候,会自动隐式解码UTF8字符串。

// 遍历字符串
func traversalString() {
	s := "pprof.cn博客"
	for i := 0; i < len(s); i++ {                   //byte
		fmt.Printf("%v(%c) ", s[i], s[i])
	}
	fmt.Println()

	for _, r := range s {                            //rune,用于遍历带有中文字符的字符串
		fmt.Printf("%v(%c) ", r, r)
	}
	fmt.Println()
}

输出:

112(p) 112(p) 114(r) 111(o) 102(f) 46(.) 99(c) 110(n) 229(å) 141() 154() 229(å) 174(®) 162(¢) 
112(p) 112(p) 114(r) 111(o) 102(f) 46(.) 99(c) 110(n) 21338() 23458() 

因为UTF8编码下一个中文汉字由3~4个字节组成,所以我们不能简单的按照字节去遍历一个包含中文的字符串,否则就会出现上面输出中第一行的结果。

字符串底层是一个byte数组,所以可以和[]byte类型相互转换。字符串是不能修改的 字符串是由byte字节组成,所以字符串的长度是byte字节的长度。 rune类型用来表示utf8字符,一个rune字符由一个或多个byte组成。

5、字符串遍历

5.1 字节长度

字符串的内容(纯字节)可以通过标准索引法来获取,在中括号 [] 内写入索引,索引从 0 开始计数:

  • 字符串 str 的第 1 个字节:str[0] 第 1 个字节

  • str[i - 1] 最后 1 个字节:str[len(str) - 1]

  • 获取字符串所占的字节长度,如:len(str)

注意:内置的len()函数获取的是每个字符的UTF-8编码的长度和,而不是直接的字符数量。

package main

import (
	"fmt"
	"unicode/utf8"
)

func main() {
  // 一个中文字符编码需要三个字节
	s := "其实就是rune"
	fmt.Println(len(s))                     // "16" ,字节数, 3 * 4 + 4 = 16 字节
	fmt.Println(utf8.RuneCountInString(s))  // "8"  ,字符数,共 8 个字符数
}

5.2 字符数量

for range循环处理字符时,不是按照字节的方式来处理的。v 其实际上是一个rune类型值。实际上,Go语言的range循环在处理字符串的时候,会自动隐式解码UTF8字符串。如字符串含有中文等字符,我们可以看到每个中文字符的索引值相差3。

package main

import (
	"fmt"
)

func main() {
	s := "Go语言四十二章经"
	for k, v := range s {
		fmt.Printf("k:%d,v:%c == %d\n", k, v, v)
	}
}

因为一个中文字符编码需要三个字节,转化单个字节会出现乱码。如字符串含有中文等字符,我们可以看到每个中文字符的索引值相差3。

程序输出:
k:0,v:G == 71
k:1,v:o == 111
k:2,v:语 == 35821
k:5,v:言 == 35328
k:8,v:四 == 22235
k:11,v:十 == 21313
k:14,v:二 == 20108
k:17,v:章 == 31456
k:20,v:经 == 32463

5.3 字符串切片

在 Go 语言中,可以通过字符串切片实现获取子串的功能,切片区间可以对比数学中的区间概念来理解,它是一个左闭右开的区间(索引值非负):

	str := "hello, golang 风清扬"
	str1 := str[:5]   // 获取索引5(不含)之前的子串
	str2 := str[7:]   // 获取索引7(含)之后的子串
	str3 := str[0:5]  // 获取从索引0(含)到索引5(不含)之间的子串
	str4 := str[:]    // 打印完整的字符串
	fmt.Println("str1:", str1)
	fmt.Println("str2:", str2)
	fmt.Println("str3:", str3)
	fmt.Println("str4:", str4)

输出:

str1: hello
str2: golang 风清扬
str3: hello
str4: hello, golang 风清扬

6、字符串修改

要修改字符串,需要先将其转换成[]rune或[]byte,完成后再转换为string。无论哪种转换,都会重新分配内存,并复制字节数组

    func changeString() {
        s1 := "hello"
        // 强制类型转换
        byteS1 := []byte(s1)
        byteS1[0] = 'H'
        fmt.Println(string(byteS1))

        s2 := "博客"
        runeS2 := []rune(s2)
        runeS2[0] = '狗'
        fmt.Println(string(runeS2))
    }

7、字符串类型转换

Go语言中只有强制类型转换,没有隐式类型转换。该语法只能在两个类型之间支持相互转换的时候使用。

强制类型转换的基本语法如下:

T(表达式)

其中,T表示要转换的类型。表达式包括变量、复杂算子和函数返回值等.

比如计算直角三角形的斜边长时使用math包的Sqrt()函数,该函数接收的是float64类型的参数,而变量a和b都是int类型的,这个时候就需要将a和b强制类型转换为float64类型。

    func sqrtDemo() {
        var a, b = 3, 4
        var c int
        // math.Sqrt()接收的参数是float64类型,需要强制转换
        c = int(math.Sqrt(float64(a*a + b*b)))
        fmt.Println(c)
    }

8、strings 包 常用操作

说明:这里说的字符,指得是 rune 类型,即一个 UTF-8 字符(Unicode 代码点)。

8.1 字符串比较

  • 对字符串的比较除了使用内置的==符号来比较,strings标准库还提供了两个方法分别是CompareEqualFold

  • 需要明确的是,字符的比较是通过查出字符对应的ASCII码值,然后进行总和的计算再进行大小的比较

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

示例:比较字符串:相等输出0、大于输出1、小于输出-1

	a := "gopher"
	b := "golang"
	fmt.Println(strings.Compare(a, b))        // 1    大于 
	fmt.Println(strings.Compare(a, a))        // 0    相等
	fmt.Println(strings.Compare(b, a))        // -1   小于 
2)EqualFold
   //   EqualFold 函数,计算 s 与 t 忽略字母大小写后是否相等。
   func EqualFold(s, t string) bool
  • EqualFold相比Compare忽略了字母大小来比较
  • 返回的结果为布尔值,true代表相等,false代表不相等
	fmt.Println(strings.EqualFold("GO", "go"))      // true
	fmt.Println(strings.EqualFold("1", "一"))       // false

8.2 查询是否存在某个字符或子串

检查的字符串中是否包含有某个字串或字符的需求,比如“abcdefg”是否包含“efg”或者‘a’,strings包主要提供了Contains的方法供开发者使用,返回值是布尔值。

1)Contains方法系

三个函数签名如下:

// 子串 substr 在 s 中,返回 true
func Contains(s, substr string) bool

// chars 中任何一个 Unicode 代码点在 s 中,返回 true
// 注意空字符的情况是返回false,与上面的Contains区分开来
func ContainsAny(s, chars string) bool

// Unicode 代码点 r 在 s 中,返回 true
func ContainsRune(s string, r rune) bool

示例:

  • Contains
	// Contains
	fmt.Println(strings.Contains("adbcadeadh", "adb"))      // true,存在
	fmt.Println(strings.Contains("adbcadeadh", "abc"))      // false,不存在
	// 注意空子串和空格子串的区别:
	fmt.Println(strings.Contains("abc", ""))            // true,存在   
	fmt.Println(strings.Contains("abc", " "))           // false,不存在
  • ContainsAny:但凡字符串s 内包含 chars 任意一个字符(Unicode Code Point) 返回 true
	// ConmtainsAny
	/*
	第二个参数 chars 中任意一个字符(Unicode Code Point)
	如果在第一个参数 s 中存在,则返回 true,否则返回false
	注意空字符的情况是返回false,与上面的Contains区分开来
	*/
	fmt.Println(strings.ContainsAny("team", "b"))          // false
	fmt.Println(strings.ContainsAny("team", "t"))          // true
	fmt.Println(strings.ContainsAny("team", "a & e "))     // true
	fmt.Println(strings.ContainsAny("team", "a | e "))     // true
	fmt.Println(strings.ContainsAny("team", "s g "))       // false
	fmt.Println(strings.ContainsAny("team", " "))          // false
	fmt.Println(strings.ContainsAny("team", ""))           // false
	fmt.Println(strings.ContainsAny("", ""))               // false
	fmt.Println(strings.ContainsAny("team", "ea"))         // true
	fmt.Println(strings.ContainsAny("team", "eg"))         // true
	fmt.Println(strings.ContainsAny("team", "ig"))         // false
	fmt.Println(strings.ContainsAny("team", "ge"))         // true
  • ContainsRune 查询单个字符的情况
	// ContainsRune
	// 查询单个字符的情况,注意 rune 为 单个字符,单引号
	fmt.Println(strings.ContainsRune("team", 'a'))           // ture
	fmt.Println(strings.ContainsRune("team", 'k'))           // false
	fmt.Println(strings.ContainsRune("team", ' '))           // false
2)Contains方法系的内部实现
  • 查看这三个函数的源码,发现它们只是调用了相应的 Index 函数(子串出现的位置)

  • 然后和 0 作比较返回 true 或 fales。

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

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

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

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

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

func Count(s, sep string) int

这里要特别说明一下的是当 sep 为空(“”)时,Count 的返回值是:utf8.RuneCountInString(s) + 1

	fmt.Println(strings.Count("cheese", "e"))         // 3
	fmt.Println(len("百度中国"))                              // 12 个 字节
	fmt.Println(strings.Count("百度中国", ""))          // 5, utf8.RuneCountInString(s) + 1
	fmt.Println(strings.Count("爱学习的你", "学"))       // 1
  • 另外,Count 是计算子串在字符串中出现的无重叠的次数,比如:
	fmt.Println(strings.Count("fiveveve", "vev"))     // 1

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

strings.Index可以在字符串中搜索某个子串,并得到对应子串起始索引下标,若不存在对应子串则返回-1。

// 在 s 中查找 sep 的第一次出现,返回第一次出现的索引
func Index(s, sep string) int

除了对子串进行搜索之外,也可以对某个字节,字符,字符集合进行搜索。

1)获取正向索引,从左往右匹配到第一个
// 字节搜索,在 s 中查找字节 c 的第一次出现,返回第一次出现的索引
func IndexByte(s string, c byte) int

// 字符搜索,Unicode 代码点 r 在 s 中第一次出现的位置
func IndexRune(s string, r rune) int

// 字符集合搜索,匹配chars中的任何一个字符
// 返回chars 中任何一个 Unicode 代码点在 s 中首次出现的位置
func IndexAny(s, chars string) int

// 查找字符 c 在 s 中第一次出现的位置,其中 c 满足 f(c) 返回 true
func IndexFunc(s string, f func(rune) bool) int

示例:

	// 匹配字符第一次出现
	fmt.Println(strings.Index("golang go go go", "g"))     // 0
	fmt.Println(strings.Index("golang go go go", "o"))     // 1

	// 匹配字节,单引号
	fmt.Println(strings.IndexByte("hello", 'o'))              // 4

	// 匹配字符,单引号
	fmt.Println(strings.IndexRune("风华正茂", '风'))           // 0

	// 匹配字符集合
	fmt.Println(strings.IndexAny("golang", "l&g"))          // 0
	fmt.Println(strings.IndexAny("golang", "&o"))           // 1
2)获取反向索引,从右往左 匹配到最后一个
  • strings包也提供了一系列函数获取对应元素的最后一个匹配项的索引下标
  • 对应于上面的每个Index函数,都有一个LastIndex函数

函数声明如下:

// 查找最后一次出现的位置
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

示例:

	// 匹配字符最后一次出现
	fmt.Println(strings.LastIndex("golang go go go", "go"))     // 13
	fmt.Println(strings.LastIndexByte("golang go go go", 'o'))     // 14

	// 匹配字节,单引号
	fmt.Println(strings.LastIndexByte("hello", 'o'))              // 4

	// 匹配字符集合
	fmt.Println(strings.LastIndexAny("golang", "l&g"))          // 5
	fmt.Println(strings.LastIndexAny("golang", "&o"))           // 1
  • 需要再次注意的是,IndexAnyLastIndexAny 返回的是Unicode码点对应的索引这
  • 比如“你”和“好”都占用3个字节,“你好”匹配“好”,返回的就是3。
	s := "你好帅!你好帅!"
	fmt.Printf("\"%s\"的字节长度:%d\n", s, len(s))        // 24
	fmt.Println(strings.LastIndexAny(s, "你"))           // 12

3)IndexFunc 和 LastIndexFunc

IndexFuncLastIndexFunc,用途是查找字符字符串中第一次出现的位置,其中字符满足 func函数 并返回 true

	// 查找字符串中汉字第一次出现的位置,没有汉字返回-1
	han := func(c rune) bool {
		return unicode.Is(unicode.Han, c)     // 汉字
	}

	// 无匹配
	fmt.Println(strings.IndexFunc("hello golang", han))        // -1

	// 匹配 "你"
	fmt.Println(strings.LastIndexFunc("你好,Golang", han))     // 3 
	// 匹配 "好"
	fmt.Println(strings.IndexFunc("你好,Golang", han))         // 0

8.5 字符串分割为[]string

字符串分割很常见,一般分割后会返回对应的字符串切片,strings包提供了六个三组分割函数:Fields 和 FieldsFunc、Split 和 SplitAfter、SplitN 和 SplitAfterN。

1)Fields 和 FieldsFunc

函数的签名如下:

func Fields(s string) []string
func FieldsFunc(s string, f func(rune) bool) []string
Fields
  • Fields一个或多个连续的空格分隔字符串 s,返回子字符串的切片。
  • 如果字符串 s 只包含空格,则返回空列表 ([]string 的长度为 0)。
  • 其中,空格的定义是 unicode.IsSpace

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

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

	fmt.Printf("Fields are: %q\n", strings.Fields("  foo bar  baz   "))      //Fields are: ["foo" "bar" "baz"]
	fmt.Printf("Fields are: %q\n", strings.Fields("  "))                     //Fields are: []
FieldsFunc
  • FieldsFunc 用这样的 Unicode 代码点 c 进行分隔:满足 f© 返回 则true,该函数返回[]string。
  • 如果字符串 s 中所有的代码点 (unicode code points) 都满足 f© 或者 s 是空,则 FieldsFunc 返回空 slice。
  • 也就是说,我们可以通过实现一个回调函数来指定分隔字符串 s 的字符。
  • 比如上面的例子,我们通过 FieldsFunc 来实现:
	// FieldsFunc are: ["foo" "bar" "baz"]
	fmt.Printf("FieldsFunc are: %q\n", strings.FieldsFunc("  foo bar  baz   ", unicode.IsSpace))
	
	// FieldsFunc are: ["a" "b" "c"]
	fmt.Printf("FieldsFunc are: %q\n", strings.FieldsFunc("a+b+c", func(r rune) bool {
		return r=='+'
	}))
  • 通过查看源码发现,实际上,Fields 函数就是调用 FieldsFunc 实现的:

    func Fields(s string) []string {
      return FieldsFunc(s, unicode.IsSpace)
    }
    
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
	fmt.Printf("%q\n", strings.Split("a,b,c,d", ","))    // ["a" "b" "c" "d"]
  fmt.Printf("%q\n", strings.Split("你是一个大帅比,你就是", "你"))    // ["" "是一个大帅比," "就是"]
  fmt.Printf("%q\n", strings.Split(" a bc", ""))     // [" " "a" " " "b" "c"]
  fmt.Printf("%q\n", strings.Split("", "xx"))        // [""]
SplitAfter

那么,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。

SplitN和SplitAfterN
  • 带 N 的方法可以通过最后一个参数 n 控制返回的结果中的 slice 中的元素个数

  • 当n < 0 时,返回所有的子字符串;

  • 当 n == 0 时,返回的结果是 nil;

  • 当 n > 0 时,表示返回的 slice 中最多只有 n 个元素,其中,最后一个元素不会分割,比如:

	// SplintN, Split 会将 s 中的 sep 去掉
	// n > 0,表示返回的 slice 中最多只有 n 个元素,其中,最后一个元素不会分割
	fmt.Printf("%q\n", strings.SplitN("cat,pig,bird", ",", 1))    // ["cat,pig,bird"]
	fmt.Printf("%q\n", strings.SplitN("cat,pig,bird", ",", 2))    // ["cat" "pig","bird"]
	fmt.Printf("%q\n", strings.SplitN("cat,pig,bird", ",", 3))    // ["cat" "pig" "bird"]
	fmt.Printf("%q\n", strings.SplitN("cat,pig,bird", ",", 4))    // ["cat" "pig" "bird"]

	// n = 0 , 返回的结果是 nil
	fmt.Printf("%q\n", strings.SplitN("cat, pig, alex", ",", 0))   // []

	// n < 0, 返回所有的子字符串
	fmt.Printf("%q\n", strings.SplitN("cat, pig, alex", ",", -1))  // ["cat" " pig" " alex"]
	// SplintAfterN,  SplitAfter 会保留 sep
	// n > 0,表示返回的 slice 中最多只有 n 个元素,其中,最后一个元素不会分割
	fmt.Printf("%q\n", strings.SplitAfterN("cat,pig,bird", ",", 1))    // ["cat,pig,bird"]
	fmt.Printf("%q\n", strings.SplitAfterN("cat,pig,bird", ",", 2))    // ["cat,","pig,bird"]
	fmt.Printf("%q\n", strings.SplitAfterN("cat,pig,bird", ",", 3))    // ["cat,","pig,","bird"]
	fmt.Printf("%q\n", strings.SplitAfterN("cat,pig,bird", ",", 4))    // ["cat,","pig,","bird"]

	// n = 0 , 返回的结果是 nil
	fmt.Printf("%q\n", strings.SplitAfterN("cat, pig, alex", ",", 0))   // []

	// n < 0, 返回所有的子字符串
	fmt.Printf("%q\n", strings.SplitAfterN("cat, pig, alex", ",", -1))  // ["cat" " pig" " alex"]

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

函数源码如下:

// 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"))         //  true
	fmt.Println(strings.HasPrefix("Gopher", "C"))          //  false
	fmt.Println(strings.HasPrefix("Gopher", ""))           //  true
	fmt.Println(strings.HasSuffix("Amigo", "go"))          //  true
	fmt.Println(strings.HasSuffix("Amigo", "Ami"))         //  false
	fmt.Println(strings.HasSuffix("Amigo", ""))            //  true

8.7 字符串 JOIN 操作

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

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

示例如下:

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

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

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 方式)。

8.8 字符串重复几次

函数签名如下:

func Repeat(s string, count int) string

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

	fmt.Println(strings.Repeat("*", 5) + " Go " + strings.Repeat("*", 5))   // ***** Go *****

8.9 字符替换

函数签名如下:

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

8.10 字符串子串替换

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

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

// 用 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))               // oinky oinky oink
fmt.Println(strings.Replace("oink oink oink", "oink", "moo", -1))          // moo moo moo
fmt.Println(strings.ReplaceAll("oink oink oink", "oink", "mmm"))           // mmm mmm mmm

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

8.11 字符串大小写转换

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

函数签名如下:

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

示例如下:

	fmt.Println(strings.ToLower("HELLO WORLD"))       // hello world
	fmt.Println(strings.ToLower("Ā Á Ǎ À"))           // ā á ǎ à

	fmt.Println(strings.ToLowerSpecial(unicode.TurkishCase, "壹"))              // 壹
	fmt.Println(strings.ToLowerSpecial(unicode.TurkishCase, "HELLO WORLD"))     // hello world

	fmt.Println(strings.ToLower("Önnek İş"))                               // önnek iş
	fmt.Println(strings.ToLowerSpecial(unicode.TurkishCase, "Önnek İş"))   // önnek iş

	fmt.Println(strings.ToUpper("hello world"))   // HELLO WORLD
	fmt.Println(strings.ToUpper("ā á ǎ à"))       // Ā Á Ǎ À

	fmt.Println(strings.ToUpperSpecial(unicode.TurkishCase, "一"))   // 一
	fmt.Println(strings.ToUpperSpecial(unicode.TurkishCase, "hello world"))  // HELLO WORLD

	fmt.Println(strings.ToUpper("örnek iş"))   //  ÖRNEK IŞ
	fmt.Println(strings.ToUpperSpecial(unicode.TurkishCase, "örnek iş"))  // ÖRNEK İŞ

8.12 字符串标题处理

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

函数签名如下:

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

举例如下:

	fmt.Println(strings.Title("hElLo wOrLd"))          // HElLo WOrLd
	fmt.Println(strings.ToTitle("hElLo wOrLd"))        // HELLO WORLD
	fmt.Println(strings.ToTitleSpecial(unicode.TurkishCase, "hElLo wOrLd"))  // 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"))
	// Dünyanın Ilk Borsa Yapısı Aizonai Kabul Edilir
	fmt.Println(strings.ToTitle("dünyanın ilk borsa yapısı Aizonai kabul edilir"))
	// DÜNYANIN ILK BORSA YAPISI AIZONAI KABUL EDILIR
	fmt.Println(strings.ToTitleSpecial(unicode.TurkishCase, "dünyanın ilk borsa yapısı Aizonai kabul edilir"))
	// DÜNYANIN İLK BORSA YAPISI AİZONAİ KABUL EDİLİR

8.13 字符串修剪

函数签名:

// 将 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###$$$
!!!@@@你好

8.14 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)

8.15 Builder 类型

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.
  • Buildercap 会自动增长,一般不需要手动调用 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你好,世界

8.16 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 两个属性来控制。

9、字符串相关补充

标准库中有四个包对字符串处理尤为重要:bytes、strings、strconv和unicode包。

  • strings包提供了许多如字符串的查询、替换、比较、截断、拆分和合并等功能。

  • bytes包也提供了很多类似功能的函数,但是针对和字符串有着相同结构的[]byte类型。因为字符串是只读的,因此逐步构建字符串会导致很多分配和复制。在这种情况下,使用bytes.Buffer类型将会更有效,稍后我们将展示。

  • strconv包提供了布尔型、整型数、浮点数和对应字符串的相互转换,还提供了双引号转义相关的转换。

  • unicode包提供了IsDigit、IsLetter、IsUpper和IsLower等类似功能,它们用于给字符分类

strings 包提供了很多操作字符串的简单函数,通常一般的字符串操作需求都可以在这个包中找到。

下面简单举几个例子:

  • 判断是否以某字符串打头/结尾 strings.HasPrefix(s, prefix string) bool strings.HasSuffix(s, suffix string) bool

  • 字符串分割 strings.Split(s, sep string) []string

  • 返回子串索引 strings.Index(s, substr string) int strings.LastIndex 最后一个匹配索引

  • 字符串连接 strings.Join(a []string, sep string) string 另外可以直接使用“+”来连接两个字符串

  • 字符串替换 strings.Replace(s, old, new string, n int) string

  • 字符串转化为大小写 strings.ToUpper(s string) string strings.ToLower(s string) string

  • 统计某个字符在字符串出现的次数 strings.Count(s, substr string) int

  • 判断字符串的包含关系 strings.Contains(s, substr string) bool

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值