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)) }
重复次数不能为负数,会引发运行时错误。