Go 语言类型 字符串

基本概念

字符串(String)是一个不可变的 UTF-8 编码字节序列,用于处理文本。

在 Go 语言中,可以把字符串理解为不能修改的字节数组,由两个部分组成:

  • 数据指针(Data Pointer):指向底层字节数组的指针,数组元素为每个字符的 UTF-8 编码字节。

  • 长度(Length):底层字节数组长度,表示字符串大小。

字符串采用这种结构有几大好处:

  • 线程安全:由于字符串不可更改,自然是线程安全的。

  • 高效传递:在函数调用中传递字符串时,只需传递指针和长度,而不需要复制整个字符串内容。

  • 内存共享:不同的字符串变量内容相同时,这些变量指向同一个底层数组,很省内存。

创建字符串

字符串可以由字面量创建或者从由切片直接转换而来。

声明和初始化

字符串字面量需要用双引号 " 括起来,支持转义字符:

package main

import "fmt"

func main() {
	// 创建空字符串
	var a string

	// 包含转义字符
	var b = "b2\t"

	// 短变量声明,使用字面量初始化
	c := "c3"

	// 赋值表达式,从字节切片转换
	d := string([]byte{100, 100})

	fmt.Println(a, b, c, d) // 输出: b2      c3 dd
}

原生字面量

原生字符串字面量使用反引号「`」括起来,里面内容会保留原始格式。使用原生字面量可以方便地在字符串中包含换行符、引号等特殊字符(除了反引号)而不用进行转义,也不会解析转义字符:

package main

import "fmt"

func main() {
	// 使用原生字面量定义字符串
	s := `第一行\t

第三行,'不'转换转义字符 \n
  第四行,"前面"带两空格`

	fmt.Println(s) // 原样输出,包括空格和空行
}

从切片构造

字节切片或字符切片可以直接转为字符串类型。也可以基于字符串切分出新字符串:

package main

import "fmt"

func main() {
	// 从字节切片转换,输出:Hello
	fmt.Println(string([]byte{72, 101, 108, 108, 111}))

	// 从字符切片转换,输出:你好世界
	fmt.Println(string([]rune{'你', '好', '世', '界'}))

	// 从字符串切分,输出:hello
	fmt.Println("hello world"[:5])
}

字符串操作

字符串本身提供长度计算、索引、切片、拼接和比较操作,更多字符串操作函数在标准库 strings 包中。

字符串索引

字符串以字节序列形式存储,所以每个索引对应字符串中的一个字节。在涉及多字节字符时,需要先转为字符切片,再按索引读取字符值:

package main

import "fmt"

func main() {
	str := "Hello, 世界!"

	// 访问字符串第一个字节
	a := str[0]
	fmt.Printf("字符串 '%s' 的第一个字节是 '%c',字节值为 %d\n", str, a, a)

	// 试图访问 Unicode 字符某个字节,导致切割字符
	b := str[7]
	fmt.Printf("在字符串 '%s' 的索引 7 处的字节是 '%c',字节值为 %d\n", str, b, b)

	// 处理多字节字符先转为字符切片后再索引
	c := []rune(str)[7]
	fmt.Printf("在字符串 '%s' 中位置 7 的字符是 '%c',字符值为 %U\n", str, c, c)

	// 对字符串中字节取址和设值都将报错
	fmt.Println(&str[0]) // 报错:cannot take the address of str[0]
	str[0] = 'w'         // 报错:cannot assign to str[0]
}

和切片类型不同,不可以获取字符串中某字节的指针地址。

字符串遍历

字符串可以用字节或字符为单位遍历:

package main

import "fmt"

func main() {
	s := "cn=中文"

	// 以字符方式遍历,分别输出 5 个字符和对应编码
	// 等同于先转为字符切片,再计算切片长度
	fmt.Println("以字符方式遍历:")
	for _, v := range s {
		fmt.Printf("字符:%c 的 Unicode 编码为:%X \n", v, v)
	}

	// 以字节方式遍历,分别输出 9 个整数
	// 中文字在 UTF-8 编码中占 3 个字节
	fmt.Println("以字节方式遍历:")
	for i := 0; i < len(s); i++ {
		fmt.Printf("第 %d 个字节的编码为:%v \n", i, s[i])
	}
}

转为字符切片来遍历涉及复制操作,只用于需要修改字符串的场合。

字符串转换

字符串和基本数据类型之间转换,通过标准库 strconv 包中函数完成。Format 开头函数用于将基本数据类型转为字符串,转换不会失败;Parse 开头函数用于将字符串转为其他基本数据类型,需要处理报错:

package main

import (
	"fmt"
	"strconv"
)

func main() {
	// 字符串转布尔型
	fmt.Println(strconv.ParseBool("true"))

	// 布尔型转字符串
	fmt.Println(strconv.FormatBool(true))

	// 字符串转浮点型 float64
	// 第二个参数代表浮点精度
	fmt.Println(strconv.ParseFloat("1.43", 64))

	// 浮点型转字符串
	// 第四个参数代表浮点精度
	// 中间两个参数用 Println 默认值
	fmt.Println(strconv.FormatFloat(2.30, 'g', -1, 32))

	// 字符串转整型
	fmt.Println(strconv.Atoi("256"))

	// 整型转字符串
	fmt.Println(strconv.Itoa(44))

	// 字符串转其他类型,可能会失败
	// 此时返回错误结果值,报错不为 nil
	// 下面指定将 16 进制数字 0x8A 转为 int8 类型
	// 返回值 127,报错:value out of range
	fmt.Println(strconv.ParseInt("8A", 16, 8))
}

如果对性能不在意,可以借助 fmt.Sprint 函数来转换基础类型到字符串:

package main

import (
	"fmt"
)

func main() {
	// 布尔型转字符串
	fmt.Println(fmt.Sprint(true))

	// 整型转字符串
	fmt.Println(fmt.Sprint(44))

	// 浮点型转字符串,小数部分不能超过 16 位,否则失真
	fmt.Println(fmt.Sprint(0.1234567890123456))
}

fmt.Sprintf 函数也常用于将字符串与其他数据类型拼接。

字符串分割

分割字符串指根据分隔符将字符串分割成若干部分,返回字符串切片:

package main

import (
	"fmt"
	"strings"
)

func main() {
	// 用逗号分割 "hello, world",输出:["hello" "   world"]
	fmt.Printf("%q\n", strings.Split("hello,   world", ","))

	// 限定分割次数,从左到右顺序分割。输出:["a" "b" "c,d,e"]
	fmt.Printf("%q\n", strings.SplitN("a,b,c,d,e", ",", 3))

	// 在分割结果中保留分隔符。输出:["a," "b," "c"]
	fmt.Printf("%q\n", strings.SplitAfterN("a,b,c", ",", 3))

	// 按空白字符分割,不仅仅是空格。输出:["a" "b" "," "c" "d"]
	fmt.Printf("%q\n", strings.Fields("a b\n, c\td"))
}

对空字符串分割,会得到空字符串切片。

字符串拼接

字符串拼接是将两个或多个字符串合并成一个新字符串。对于少量的字符串拼接,可以直接使用 + 运算符:

package main

import "fmt"

func main() {
	s := "Hello, World!"

	// 两个字符串直接拼接
	m := s[:7] + "Go!"
	fmt.Println(m) // 输出 "Hello, Go!"

	// 采用追加操作符来级联追加
	s += m
	fmt.Println(s) // 输出 "Hello, World!Hello, Go!"
}

还可以利用函数 strings.Join 在字符切片元素间插入新分隔符,重新组合成新字符串:

package main

import (
	"fmt"
	"strings"
)

func main() {
	a := "This is a test with function strings"

	// 使用 strings.Fields 将字符串分割成单词切片
	b := strings.Fields(a)
	fmt.Println("字符切片:", b)

	// 使用 strings.Join 将单词切片连接成一个新字符串,单词之间用 "?" 分隔
	c := strings.Join(b, "?")
	fmt.Printf("新字符串:%s\n", c) // 输出:This?is?a?test?with?function?strings
}

字符串修改

由于字符串不可修改,因此字符串常需要转为字节或字符切片类型来处理:

package main

import "fmt"

func main() {
	s := "Hello, 世界!"

	// 转为字节切片
	bytes := []byte(s)
	fmt.Println("字节数组:", bytes)
	fmt.Printf("字节输出: ")
	for _, b := range bytes {
		fmt.Printf("%x ", b)
	}
	fmt.Println()

	// 转为字符切片
	runes := []rune(s)
	fmt.Println("字符数组:", runes)
	fmt.Printf("字符输出: ")
	for _, r := range runes {
		fmt.Printf("%q ", r)
	}
	fmt.Println()

	// 修改字符切片后转回字符串
	runes[7] = 'G'
	runes = append(runes[:8], 'o', '!')
	fmt.Println("字符串修改:", string(runes)) // 输出 "Hello, Go!"
}

当需要修改的字符串较大或修改操作频繁时,推荐使用 strings.Builder。这个在 Go 1.10 版本引入的类型类似于 bytes.Buffer 缓冲,但针对字符串做过优化,减少了内存分配和复制操作:

package main

import (
	"bytes"
	"fmt"
	"strings"
)

func main() {
	words := []string{"hello", "world"}

	// 使用 Builder 来创建或修改字符串
	var builder strings.Builder
	for _, v := range words {
		if v == "world" {
			builder.WriteString("go")
			builder.WriteRune('!') // 使用 WriteRune 写入字符
		} else {
			builder.WriteString(v) // 使用 WriteString 写入字符串
			builder.WriteString(" ")
		}
	}
	result := builder.String() // 使用 String 转为字符串
	fmt.Println(result)        // 输出:hello go!

	// 使用缓冲来创建字符串,方法更通用
	var buffer bytes.Buffer
	buffer.WriteString("Hello, World!")
	buffer.Truncate(7) // 只保留前 7 个字节
	buffer.WriteString("Go!")
	fmt.Println(buffer.String()) // 转为字符串,输出 "Hello, Go!"
}

字符串统计

内置 len 函数默认统计字符串字节数大小。要统计字符数,需要先转为字符切片再计算长度,或者直接使用 utf8.RuneCountInString 函数:

package main

import (
	"fmt"
	"unicode/utf8"
)

func main() {
	s := "Hello, 世界!"

	fmt.Printf("%d\n", len(s))             // 统计字符串字节大小为 14
	fmt.Printf("%d\n", len([]rune(s)))     // 转换成字符切片后,统计得到字符数 10
	fmt.Println(utf8.RuneCountInString(s)) // 直接得到字符数 10
}

要统计指定字符串出现次数,可以用 strings.Count 函数:

package main

import (
	"fmt"
	"strings"
)

func main() {
	s := "你好世界,你好"
	fmt.Println(strings.Count(s, "你好")) // 返回次数 2
}

字符串比较

字符串可以进行比较运算,大小比较判断依据的是字符字典序:

package main

import "fmt"

func main() {
	a := "Hello"
	b := "hello"

	// 一般直接用相等性比较
	fmt.Println("a 等于 b:", a == b)

	// 使用 < 或 > 比较字符字典序
	fmt.Println("a 小于 b:", a < b)
	fmt.Println("a:", a[0], "b:", b[0])
}

想在比较时忽略字母大小写,可以使用 strings.EqualFold 函数:

package main

import (
	"fmt"
	"strings"
)

func main() {
	// 使用 strings.EqualFold 比较字符串不区分大小写
	fmt.Println("str1 等于 str2:", strings.EqualFold("Hello", "hello"))
}

字符串搜索

字符串搜索是查找子字符串在原字符串中是否存在或所在位置,空字符串被视为所有字符串的子串。

搜索字符串内容,返回布尔值结果:

package main

import (
	"fmt"
	"strings"
)

func main() {
	s := "The quick brown fox jumps over the lazy dog"
	fmt.Println(strings.HasSuffix(s, " dog"))       // 匹配后缀,返回 true
	fmt.Println(strings.HasPrefix(s, "The quick ")) // 匹配前缀,返回 true
	fmt.Println(strings.Contains(s, "fox"))         // 搜索整个字符串,返回 true
	fmt.Println(strings.ContainsAny(s, "Pa"))       // P 不在但是 a 在字符串中,返回 true

	// 调用 Contains 检查 s 中是否包含空字符串
	fmt.Println(strings.Contains(s, ""))    // 返回 true
	// 而 ContainsAny 检查 s 中字符是否出现在目标字符中,空字符串视为没有字符要检查
	fmt.Println(strings.ContainsAny(s, "")) // 返回 false
}

搜索字符串所在位置索引,没找到时返回 -1

package main

import (
	"fmt"
	"strings"
)

func main() {
	s := "012连接abc"
	// 索引始于 0。1 个中字占 3 索引,所以返回 9
	fmt.Println(strings.Index(s, "abc"))

	// 从字符串末尾开始搜索,返回 9
	fmt.Println(strings.LastIndex(s, "abc"))

	// 通过字符搜索,返回 6
	fmt.Println(strings.IndexRune(s, '接'))
}

字符串替换

字符串简单替换要求,可以使用 strings.Replace 函数。函数接收 4 个参数:原字符串、被替换内容、替换内容、替换次数,返回新字符串。如果要全量替换,替换次数传入 -1

package main

import (
	"fmt"
	"strings"
)

func main() {
	s := "Hello Go lang. Go lang is fun."

	// 替换第一次出现的 Go lang 为 Golang,输出:Hello Golang. Go lang is fun.
	fmt.Println("替换一次:", strings.Replace(s, "Go lang", "Golang", 1))

	// 全部替换专用函数,输出:Hello Golang. Golang is fun.
	fmt.Println("全部替换:", strings.ReplaceAll(s, "Go lang", "Golang"))
}

strings.Map 函数能实现高级替换功能,函数接受一个签名为 func(rune) rune 的映射函数,对字符串中每个字符都调用映射函数处理:

package main

import (
	"fmt"
	"strings"
	"unicode"
)

func main() {
	s := "Hello, World! 123 Go!"

	// 匿名函数,转换所有字符为大写
	upper := func(r rune) rune {
		return unicode.ToUpper(r)
	}

	// 匿名函数,删除字符串中所有数字
	clear := func(r rune) rune {
		if unicode.IsDigit(r) {
			return -1 // 返回 -1 表示删除此字符
		}
		return r
	}

	// 输出:Hello, World!  Go!
	fmt.Println("转为大写,删除数字:", strings.Map(clear, strings.Map(upper, s)))
}

大小写转换

多个包中都有 ToLowerToUpper 函数,strings 包中的能对整个字符串转换大小写:

package main

import (
	"fmt"
	"strings"
)

func main() {
	s := "Guten Tag! ¿Cómo estás?"

	// 转换为大写,输出:GUTEN TAG! ¿CÓMO ESTÁS?
	fmt.Println("转换为大写:", strings.ToUpper(s))

	// 转换为小写,输出:guten tag! ¿cómo estás?
	fmt.Println("转换为小写:", strings.ToLower(s))
}

本用于转首字母大写的函数 strings.Title,会将字符串全部转为大写,不能使用。

字符串修剪

字符串修剪指去除字符串中一些多余字符,例如字符串首尾的空格:

package main

import (
	"fmt"
	"strings"
)

func main() {
	fmt.Println(strings.Trim("?a?b?c?", "?ac")) // 剔除首尾的 ? 号和 ac,输出:b
	fmt.Println(strings.TrimSpace(" a\tb  \n")) // 剔除首尾空白字符,输出:a       b

	fmt.Println(strings.TrimLeft("??a??", "?"))  // 只剔除左边 ? 号,输出:a??
	fmt.Println(strings.TrimRight("??a??", "?")) // 只剔除右边 ? 号,输出:??a

	fmt.Println(strings.TrimPrefix("??a??", "?")) // 只移除左边 ? 号,输出:?a??
	fmt.Println(strings.TrimSuffix("??a??", "?")) // 只移除右边 ? 号,输出:??a?
}

字符串重复

strings.Repeat 函数将字符串重复指定次数,返回新字符串:

package main

import (
	"fmt"
	"strings"
)

func main() {
	// 输出:SOS! SOS! SOS!
	fmt.Println(strings.Repeat("SOS! ", 3))
}

重复次数不能为负数,会引发运行时错误。