4. 字符串

上一章介绍了 Go 语言中内置的三种常用复合数据类型:数组(array)、切片(slice)和字典(map),这三种数据结构都可以用于保存多项数据,其中数组和切片较为相似,都可以按顺序保存元素,并可以通过索引访问元素,不同的是数组的长度是不可修改的,而切片可通过增加、删除等修改改变切片的长度。字典中存储的则是 key-value 类型的数据。本章要介绍的字符串也是 Go 语言中的中重要的一种数据结构,在 Go 语言中,字符串就是一段 UTF-8 编码的只读字节序列。

4.1. 字符串的定义

字符串的定义与其他变量的定义相同,使用关键字 var 声明一个变量,格式为:var identifier string,如下:

var a string = "Hello World"

也可以简写成如下:

a := "Hello World"

在上面也提到,在 Go 语言中,字符串就是一段 UTF-8 编码的只读字节序列。这段表述里面,有两点需要特别注意的,分别是:字符串是只读的,另一个是字符串是字节序列,这两点会在后面详细介绍。

4.2. 字符串中的常用方法

4.2.1. 字符串长度

要想获取字符串的长度,可以使用 len() 函数,如下:

func main() {
	a := "hello"
	b := "中国"

	fmt.Println(len(a), len(b)) // 5 6
}

上面的代码,字符串 a 的长度为 5,这个符合预期,但是字符串 b 的长度为 6,这与想象中并不一样。这是因为在 Go 语言中,字符串是字节序列,因此,统计的长度是字节的个数,对于中文,一个中文字符占了 3 个字节。

4.2.2. 字符串索引和遍历

因为在 Go 语言中,字符串是字节序列,因此直接对字符串索引 [] 得到的是字节。如下代码:

func main() {
	a := "hello"
	b := "中国"

	fmt.Println(a[0]) // 104
	fmt.Println(b[0]) // 228
}

同样,直接遍历,得到的也是对应的字节,如下代码:

func main() {
	b := "中国"

	for i:=0; i<len(b); i++ {
		fmt.Printf("%d ", b[i]) // 228 184 173 229 155 189
	}
}

那么要想按照字符遍历,该如何操作呢?在 Go 语言中定义了 rune 格式,rune 格式的定义如下:

type rune = int32

同时,可以使用 for ... range 遍历,代码如下:

func main() {
	b := "中国"

	for i, r := range b {
		fmt.Println(i, r, string(r))
	}
}

运行后,显示为:

0 200133 22269

为什么 r 打印的是 2001322269?这是因为 rune 本身是 int32,需要使用 string() 函数将其转换成字符串。

4.2.3. 字符串、[]byte 以及 []rune

要想修改字符串怎么办,直接通过索引修改是不行的,如下代码:

func main() {
	a := "Hello"

	a[0] = 'h'
}

你会发现会报“cannot assign to a[0] (neither addressable nor a map index expression)”的错误。这就说明字符串是只读的,不支持直接的修改,要想修改字符串,可以将其转换成 []byte 或者 []rune,如下:

func main() {
	a := "Hello"
	a_1 := []byte(a)
	// 修改
	a_1[0] = 'h'
	// 转换成字符串
	b := string(a_1)
	fmt.Println(b) // hello
}

但是如果遇上 Unicode 字符时,就会出问题了,如下:

func main() {
	a := "中国"
	a_1 := []byte(a)
	// 修改
	a_1[0] = '美'
	// 转换成字符串
	b := string(a_1)
	fmt.Println(b)
}

此时会报错:“cannot use ‘美’ (untyped rune constant 32654) as byte value in assignment (overflows)”,这时候需要使用 []rune,代码如下:

func main() {
	a := "中国"
	a_1 := []rune(a)
	// 修改
	a_1[0] = '美'
	// 转换成字符串
	b := string(a_1)
	fmt.Println(b)
}

4.3. strings 包

除了上述介绍到的一些方法外,字符串的其他相关方法都集成在 strings 包中。

4.3.1. 字符串的拼接

在 Go 语言中,由于 string 类型是只读的,如果想要拼接字符串,直接使用“+”,每次拼接都会创建新的字符串,多次拼接会严重影响效率,因此,在 strings 包中提供了 strings.Builder 类型,通过 WriteString() 方法快速拼接字符串,代码如下:

package main

import (
	"fmt"
	"strings"
)

func main() {
	var builder strings.Builder

	builder.WriteString("Hello")
	builder.WriteString(" ")
	builder.WriteString("World")

	a := builder.String()

	fmt.Println(a) // Hello World
}

另一类是多个字符串的切片的拼接,可以使用函数strings.Join(parts, sep),代码如下:

func main() {
	s := []string{"Hello", "World"}
	r := strings.Join(s, " ")
	fmt.Println(r)
}

4.3.2. 切分字符串

字符串切分是一个非常重要的功能,在 strings 包中提供了 strings.Split(s, sep) 方法对字符串 s 按照分隔符 sep 切分,代码如下:

func main() {
	s := "a,b,c"

	var res []string = strings.Split(s, ",")

	for i:=0; i< len(res);i++ {
		fmt.Println(res[i])
	}
}

切分完 res 的数据格式是字符串数组 []string。除了 strings.Split(s, sep) 方法外,还有一类特殊的拆分,即按连续空白字符拆分,函数为:strings.Fields(s),代码如下:

func main() {
	s := "a b\tc"

	var res []string = strings.Fields(s)

	for i:=0; i< len(res);i++ {
		fmt.Println(res[i])
	}
}

4.3.3. 去除首尾指定字符

可以通过 strings.Trim(s, cutset) 函数去除字符串 s 的首尾指定字符 cutset,代码如下:

func main() {
	s := " hello world\n"

	s = strings.Trim(s, " ") // 去除首尾的空格
	s = strings.Trim(s, "\n") // 去除首尾的换行
	
	fmt.Println(s)
}

还有一类特殊的函数用于去除首尾的空白,函数为:strings.TrimSpace(s),代码如下:

func main() {
	s := " hello world  "

	s = strings.TrimSpace(s)
	
	fmt.Println(s)
}

4.3.4. 查找子串

查找子串也是较为常见的字符串操作,在 strings 包中,查找子串的函数较多,如下:

  • 查找是否包含子串:strings.Contains(s, sub)
  • 查找是否以指定前缀开头:strings.HasPrefix(s, prefix)
  • 查找是否以指定后缀结尾:strings.HasSuffix(s, suffix)

另一类是查找的过程中索引出现的位置,如下:

  • 返回子串第一次出现的位置,找不到返回 -1strings.Index(s, sub)
  • 返回最后一次出现的位置:strings.LastIndex(s, sub)

另外,还有统计子串出现次数:strings.Count(s, sub)

代码如下:

func main() {
	s := "hello world"
	fmt.Println(strings.Contains(s, "hello")) // true
	fmt.Println(strings.HasPrefix(s, "he")) // true
	fmt.Println(strings.HasSuffix(s, "ld")) // true
	fmt.Println(strings.Index(s, "o")) // 4
	fmt.Println(strings.LastIndex(s, "o")) // 7
	fmt.Println(strings.Count(s, "o")) // 2
}

4.3.5. 转换大小写

还有将字符串统一转换成大写和全部转换成小写的函数:strings.ToUpper(s)strings.ToLower(s),代码如下:

func main() {
	s := "hello world"
	a := strings.ToUpper(s)
	fmt.Println(a)
	b := strings.ToLower(a)
	fmt.Println(b)
}

4.4. 本章小结

本章介绍了 Go 语言中字符串的特点:只读和字节序列。其中字节序列的特点就决定了字符串的长度是按照字节统计的,而非按照字符统计。同时,还介绍了字符串中常用的一些方法,包括索引,遍历,以及 strings 包中带来的一些方法,如字符串拼接,切分字符串,去除首尾指定字符串等。