介绍
Go 的字符串(string)是原生类型,底层是只读字节切片,默认采用 UTF-8 编码。
注意:字符串不可修改,存储的是字节序列,不是字符。
1. 定义
var s string = "hello"
s2 := `这是原始字符串,可以换行,不转义\n`
两种字面量:
- 双引号
" ":解析转义字符\n \t \",不能直接换行 - 反引号
`:原始字符串,原样输出,不转义,支持多行
底层结构(runtime/string.go)
type string struct {
ptr unsafe.Pointer // 指向字节数组的指针
len int // 字节长度
}
- string 本身是一个结构体值,赋值时拷贝
ptr和len,底层字节数组共享 - 底层字节数组只读:不能
s[0] = 'a',编译报错
2. 字节 vs 字符
Go string 存字节。
- 英文字母:UTF-8 占 1 字节
- 中文汉字:UTF-8 占 3 字节
s := "Go语言"
fmt.Println(len(s)) // 输出 5:G(1)+o(1)+语(3)+言(3) = 8
len(s) 返回字节长度,不是字符个数。
rune字符
rune:Go 的字符类型,int32 别名,代表一个 Unicode 码点。
把字符串转 rune 切片获取字符数量:
r := []rune(s)
fmt.Println(len(r)) // 4,一共4个字符
遍历字符串两种方式
普通 for 循环(按字节索引)
s := "语言" for i := 0; i < len(s); i++ { fmt.Printf("%d: %c\n", i, s[i]) // s[i] 是 byte,字节 }中文会输出乱码,因为索引定位到字节,不是完整字符。
for range(按 Unicode 码点遍历,推荐)
s := "语言" for idx, r := range s { fmt.Printf("字符位置%d, rune:%c\n", idx, r) }range 返回的
idx是字节偏移量,不是字符序号。
3. 字符串不可变性
s := "abc"
s[0] = 'x' // ❌ 编译错误:string is immutable
想要修改字符串:转成 []byte 修改,再转回 string
s := "abc"
b := []byte(s)
b[0] = 'x'
s = string(b)
注意:转换会拷贝底层数据,产生新内存。
4. 常用操作
4.1 拼接
+最简单,少量拼接用;每次+生成新字符串,大量拼接性能差a := "hello" + " " + "world"strings.Builder✅ 大量拼接首选,内部维护字节切片,减少内存拷贝var sb strings.Builder sb.WriteString("hello") sb.WriteString(" world") res := sb.String()重要:
strings.Builder不要拷贝,一旦拷贝底层共享数组会 panic。fmt.Sprintf:格式化拼接,性能一般,适合带格式化场景s := fmt.Sprintf("name=%s, age=%d", "张三", 20)strings.Join:字符串切片拼接arr := []string{"a", "b", "c"} res := strings.Join(arr, ",") // "a,b,c"
4.2 字符串包 strings(标准库)
import "strings"
常用函数:
strings.Contains(s, substr string) bool是否包含子串strings.HasPrefix(s, prefix string) bool前缀strings.HasSuffix(s, suffix string) bool后缀strings.Index(s, substr string) int返回子串起始字节索引,找不到返回-1strings.LastIndex最后出现位置strings.Split(s, sep string) []string分割strings.ReplaceAll(s, old, new string) string全部替换strings.ToUpper / ToLower大小写转换strings.TrimSpace(s string)去除首尾空白(空格、换行、tab)strings.Trim(s, cutset string)去除首尾指定字符集
4.3 类型转换
- string ↔ []byte
s := "test" b := []byte(s) // string -> []byte,拷贝字节 s2 := string(b) // []byte -> string,拷贝字节 - string ↔ []rune
r := []rune("中文") s := string(r)
4.4 子串切片
字符串切片 s[low:high],切的是字节区间,不是字符!
s := "Go语言"
sub := s[0:2] // "Go"
sub2 := s[2:5] // "语"(3字节)
⚠️ 如果切在中文UTF-8字节中间,得到的子串是无效UTF-8,会出现乱码。
5. 内存与性能关键点
- 字符串比较
==:直接比较底层字节,高效;区分大小写 - 字符串切片
s[a:b]:不拷贝底层字节,只创建新 string 结构体(ptr+len),共享原字节数组。坑:如果原字符串很大,切一个很短子串保留引用,大内存不会被GC释放。
示例:
big := strings.Repeat("a", 1024*1024) // 1MB字符串
small := big[:5] // small 共享big底层数组,big内存无法GC
解决:需要独立内存时,转 []byte 再转回 string,强制拷贝:
small := string([]byte(big[:5]))
- 空字符串:
"",底层ptr=nil, len=0,占用极少内存 - 字符串常量:编译期确定,放在只读内存段
6. UTF-8 相关标准库
unicode / unicode/utf8
import "unicode/utf8"
utf8.RuneCountInString(s) // 获取字符数量(等价 len([]rune(s)),但不创建切片,性能更好)
utf8.DecodeRuneInString(s) // 读取第一个rune和字节长度
7. 常见坑总结
len(s)是字节数,不是汉字/字符数s[i]取的是 byte,不是 rune;中文不能用索引直接取字符s[low:high]按字节切片,截断在多字节字符中间 → 乱码- string 不可变,
s+大量拼接会频繁分配内存,优先strings.Builder - 字符串切片共享底层数组,长字符串截取短子串会内存泄漏
- range 遍历得到的 index 是字节偏移,不是字符序号
8. 例子
package main
import (
"fmt"
"strings"
"unicode/utf8"
)
func main() {
s := "Go语言"
fmt.Println("字节长度:", len(s))
fmt.Println("字符数量:", utf8.RuneCountInString(s))
// range遍历
for offset, r := range s {
fmt.Printf("字节偏移:%d, 字符:%c\n", offset, r)
}
// 拼接
var sb strings.Builder
sb.WriteString("hello")
sb.WriteString(" 世界")
fmt.Println(sb.String())
// 判断包含
fmt.Println(strings.Contains(s, "语言"))
}
如果你需要,我可以单独整理一份 Go字符串面试题清单 或者 strings包完整API速查表。