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)))
}
大小写转换
多个包中都有 ToLower
和 ToUpper
函数,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))
}
重复次数不能为负数,会引发运行时错误。