string
几乎任何程序都离不开文本(字符串)。Go 中 string 是内置类型,同时它与普通的 slice 类型有着相似的性质,例如,可以进行切片(slice)操作,这使得 Go 中少了一些处理 string 类型的函数,比如没有substring
这样的函数,然而却能够很方便的进行这样的操作。同时, string 是不可变的(只读),因此无法修改,只能使用len()
获取长度,无法使用cap()
获取容量。除此之外,Go 标准库中有几个包专门用于处理文本。
strings
包提供了很多操作字符串的简单函数,通常一般的字符串操作需求都可以在这个包中找到。strconv
包提供了基本数据类型和字符串之间的转换。在 Go 中,没有隐式类型转换,一般的类型转换可以这么做:int32(i)
,将i
(比如为 int 类型)转换int32
,然而,字符串类型和int、float、bool
等类型之间的转换却没有这么简单。
跟 C 对比
Go 往往会与 C 比较,毕竟有相同的创造者。可以说 Go 的 string 就是为了解决 C 使用字符串的困难。
下面就来列出 Go string 的特点及对比 C 的改进:
- Go string 本身就是最大的改进,因为 C 没有,只能用字符数组替代;
- 求长度是 O(1),因为有 len 字段,而 C 是 O(n),因为需要遍历整个字符数组;
- Go string 不可变,C的字符数组是可变的;
- 支持
==, !=, >, <, >=, <=
比较关系操作符; - 支持
+, +=
操作符进行字符串连接; - 每个字符都是 Unicode 字符,以 utf-8 编码存储;
- 支持多行字符串的声明。
源码阅读
Go 标准库 builtin 给出了所有内置类型的定义。 源代码位于src/builtin/builtin.go
,其中关于 string 的描述如下:
// string is the set of all strings of 8-bit bytes, conventionally but not
// necessarily representing UTF-8-encoded text. A string may be empty, but
// not nil. Values of string type are immutable.
type string string
所以 string 是 8 比特字节的集合,通常但并不一定是 UTF-8 编码的文本。
另外,还提到了两点,非常重要:
- string 可以为空(长度为0),但不会是 nil;
- string 对象不可以修改。
在 runtime 包下的 string 的结构体为:
type stringStruct struct {
str unsafe.Pointer
len int
}
其数据结构很简单:
stringStruct.str
:字符串的首地址;stringStruct.len
:字符串的长度。
string数据结构跟切片有些类似,只不过切片还有一个表示容量的成员,事实上 string 和切片,准确的说是 byte 切片经常发生转换。
在 reflect 包下的 string 的结构体为:
// StringHeader is the runtime representation of a string.
// It cannot be used safely or portably and its representation may
// change in a later release.
// Moreover, the Data field is not sufficient to guarantee the data
// it references will not be garbage collected, so programs must keep
// a separate, correctly typed pointer to the underlying data.
type StringHeader struct {
Data uintptr
Len int
}
两者有相似之处,但不一样。从StringHeader
的注释中可以看出: StringHeader
是 string 在运行时的表示形式,它本身不存储 string 的数据,而只包含一个指向 string 底层存储的指针(Data uinptr)和一个表示 string 长度的 int 字段(Len int),string 的底层存储是一个 byte 类型的数组。
关于uintptr
Go 语言规范 https://golang.org/ref/spec 中是这样描述的,an unsigned integer large enough to store the uninterpreted bits of a pointer value,是一个无符号整形大到可以存储任意一个指针的值。对比 uint 是和平台相关的无符号整形,在32位机器上是32位无符号整形,在64位机器上是64位无符号整形。
常用操作
字符串比较
Compare 函数,用于比较两个字符串的大小,如果两个字符串相等,返回为 0。如果 a 小于 b ,返回 -1 ,反之返回 1 。不推荐使用这个函数,直接使用 == != > < >= <= 等一系列运算符更加直观。
func Compare(a, b string) int
EqualFold 函数,计算 s 与 t 忽略字母大小写后是否相等。
func EqualFold(s, t string) bool
是否存在某个字符或子串
// 子串 substr 在 s 中,返回 true
func Contains(s, substr string) bool
// chars 中任何一个 Unicode 代码点在 s 中,返回 true
func ContainsAny(s, chars string) bool
// Unicode 代码点 r 在 s 中,返回 true
func ContainsRune(s string, r rune) bool
子串出现次数(字符串匹配)
在数据结构与算法中,可能会讲解以下字符串匹配算法:
- 朴素匹配算法
- KMP 算法
- Rabin-Karp 算法
- Boyer-Moore 算法
还有其他的算法,这里不一一列举,感兴趣的可以网上搜一下。
在 Go 中,查找子串出现次数即字符串模式匹配,实现的是Rabin-Karp
算法。Count 函数如下:
func Count(s, sep string) int
字符串分割为[]string
strings 包提供了六个三组分割函数:Fields 和 FieldsFunc、Split 和 SplitAfter、SplitN 和 SplitAfterN。
Fields 用一个或多个连续的空格分隔字符串 s,返回子字符串的数组(slice)。如果字符串 s 只包含空格,则返回空列表 ([]string 的长度为 0)。其中,空格的定义是 unicode.IsSpace,之前已经介绍过。
fmt.Printf("Fields are: %q", strings.Fields(" foo bar baz "))
Fields are: ["foo" "bar" "baz"]
FieldsFunc 用这样的 Unicode 代码点 c 进行分隔:满足 f(c) 返回 true。该函数返回[]string。如果字符串 s 中所有的代码点 (unicode code points) 都满足 f(c) 或者 s 是空,则 FieldsFunc 返回空 slice。
fmt.Println(strings.FieldsFunc(" foo bar baz ", unicode.IsSpace))
实际上,Fields 函数就是调用 FieldsFunc 实现的:
func Fields(s string) []string {
return FieldsFunc(s, unicode.IsSpace)
}
Split 和 SplitAfter、 SplitN 和 SplitAfterN 它们都是通过一个同一个内部函数来实现的。
func Split(s, sep string) []string { return genSplit(s, sep, 0, -1) }
func SplitAfter(s, sep string) []string { return genSplit(s, sep, len(sep), -1) }
func SplitN(s, sep string, n int) []string { return genSplit(s, sep, 0, n) }
func SplitAfterN(s, sep string, n int) []string { return genSplit(s, sep, len(sep), n) }
它们底层实现都调用了genSplit
函数。
Split(s, sep)
和SplitN(s, sep, -1)
等价;SplitAfter(s, sep)
和SplitAfterN(s, sep, -1)
等价。
Split
会将 s 中的 sep 去掉,而SplitAfter
会保留 sep。如下:
fmt.Printf("%q\n", strings.Split("foo,bar,baz", ","))
fmt.Printf("%q\n", strings.SplitAfter("foo,bar,baz", ","))
["foo" "bar" "baz"]
["foo," "bar," "baz"]
字符串是否有某个前缀或后缀
// s 中是否以 prefix 开始
func HasPrefix(s, prefix string) bool {
return len(s) >= len(prefix) && s[0:len(prefix)] == prefix
}
// s 中是否以 suffix 结尾
func HasSuffix(s, suffix string) bool {
return len(s) >= len(suffix) && s[len(s)-len(suffix):] == suffix
}
如果 prefix 或 suffix 为 "" , 返回值总是 true。
fmt.Println(strings.HasPrefix("Gopher", "Go"))
fmt.Println(strings.HasPrefix("Gopher", "C"))
fmt.Println(strings.HasPrefix("Gopher", ""))
true
false
true
字符或子串在字符串中出现的位置
// 在 s 中查找 sep 的第一次出现,返回第一次出现的索引
func Index(s, sep string) int
// 在 s 中查找字节 c 的第一次出现,返回第一次出现的索引
func IndexByte(s string, c byte) int
// chars 中任何一个 Unicode 代码点在 s 中首次出现的位置
func IndexAny(s, chars string) int
// 查找字符 c 在 s 中第一次出现的位置,其中 c 满足 f(c) 返回 true
func IndexFunc(s string, f func(rune) bool) int
// Unicode 代码点 r 在 s 中第一次出现的位置
func IndexRune(s string, r rune) int
// 有三个对应的查找最后一次出现的位置
func LastIndex(s, sep string) int
func LastIndexByte(s string, c byte) int
func LastIndexAny(s, chars string) int
func LastIndexFunc(s string, f func(rune) bool) int
字符串 JOIN 操作
将字符串数组(或 slice)连接起来可以通过 Join 实现:
fmt.Println(Join([]string{"name=xxx", "age=xx"}, "&"))
name=xxx&age=xx
字符替换
Map 函数,将 s 的每一个字符按照 mapping 的规则做映射替换,如果 mapping 返回值 <0 ,则舍弃该字符。该方法只能对每一个字符做处理,但处理方式很灵活,可以方便的过滤,筛选汉字等。
mapping := func(r rune) rune {
switch {
case r >= 'A' && r <= 'Z': // 大写字母转小写
return r + 32
case r >= 'a' && r <= 'z': // 小写字母不处理
return r
case unicode.Is(unicode.Han, r): // 汉字换行
return '\n'
}
return -1 // 过滤所有非字母、汉字的字符
}
fmt.Println(strings.Map(mapping, "Hello你#¥%……\n('World\n,好Hello^(&(*界gopher..."))
hello
world
hello
gopher
字符串子串替换
进行字符串替换时,考虑到性能问题,能不用正则尽量别用,应该用这里的函数。
// 用 new 替换 s 中的 old,一共替换 n 个。
// 如果 n < 0,则不限制替换次数,即全部替换
func Replace(s, old, new string, n int) string
// 该函数内部直接调用了函数 Replace(s, old, new , -1)
func ReplaceAll(s, old, new string) string
大小写转换
func ToLower(s string) string
func ToLowerSpecial(c unicode.SpecialCase, s string) string
func ToUpper(s string) string
func ToUpperSpecial(c unicode.SpecialCase, s string) string
修剪
// 将 s 左侧和右侧中匹配 cutset 中的任一字符的字符去掉
func Trim(s string, cutset string) string
// 将 s 左侧的匹配 cutset 中的任一字符的字符去掉
func TrimLeft(s string, cutset string) string
// 将 s 右侧的匹配 cutset 中的任一字符的字符去掉
func TrimRight(s string, cutset string) string
// 如果 s 的前缀为 prefix 则返回去掉前缀后的 string , 否则 s 没有变化。
func TrimPrefix(s, prefix string) string
// 如果 s 的后缀为 suffix 则返回去掉后缀后的 string , 否则 s 没有变化。
func TrimSuffix(s, suffix string) string
// 将 s 左侧和右侧的间隔符去掉。常见间隔符包括:'\t', '\n', '\v', '\f', '\r', ' ', U+0085 (NEL)
func TrimSpace(s string) string
// 将 s 左侧和右侧的匹配 f 的字符去掉
func TrimFunc(s string, f func(rune) bool) string
// 将 s 左侧的匹配 f 的字符去掉
func TrimLeftFunc(s string, f func(rune) bool) string
// 将 s 右侧的匹配 f 的字符去掉
func TrimRightFunc(s string, f func(rune) bool) string
高阶操作
Replacer 类型
这是一个结构,没有导出任何字段,实例化通过func NewReplacer(oldnew ...string) *Replacer
函数进行,其中不定参数 oldnew 是 old-new 对,即进行多个替换。
r := strings.NewReplacer("<", "<", ">", ">")
fmt.Println(r.Replace("This is <b>HTML</b>!"))
This is <b>HTML</b>!
Builder 类型
type Builder struct {
addr *Builder // of receiver, to detect copies by value
buf []byte
}
该类型实现了 io 包下的 Writer, ByteWriter, StringWriter 等接口,可以向该对象内写入数据,Builder 没有实现 Reader 等接口,所以该类型不可读,但提供了 String 方法可以获取对象内的数据。
// 该方法向 b 写入一个字节
func (b *Builder) WriteByte(c byte) error
// WriteRune 方法向 b 写入一个字符
func (b *Builder) WriteRune(r rune) (int, error)
// WriteRune 方法向 b 写入字节数组 p
func (b *Builder) Write(p []byte) (int, error)
// WriteRune 方法向 b 写入字符串 s
func (b *Builder) WriteString(s string) (int, error)
// Len 方法返回 b 的数据长度。
func (b *Builder) Len() int
// Cap 方法返回 b 的 cap。
func (b *Builder) Cap() int
// Grow 方法将 b 的 cap 至少增加 n (可能会更多)。如果 n 为负数,会导致 panic。
func (b *Builder) Grow(n int)
// Reset 方法将 b 清空 b 的所有内容。
func (b *Builder) Reset()
// String 方法将 b 的数据以 string 类型返回。
func (b *Builder) String() string
转 slice 的问题
我们先看一个例子:
func main() {
fmt.Println(testing.AllocsPerRun(1, convert1)) // 输出1
fmt.Println(testing.AllocsPerRun(1, convert2)) // 输出0
}
func convert1() {
s := "中国欢迎您,北京欢迎您"
sl := []byte(s)
for _, v := range sl {
_ = v
}
}
func convert2() {
s := "中国欢迎您"
sl := []byte(s)
for _, v := range sl {
_ = v
}
}
为什么convert1
方法产生了一次内存拷贝,而convert2
方法没有产生内存拷贝?
因为对于字节长度小于 32 的字符串,不会进行内存拷贝。以下为源码部分:
// The constant is known to the compiler.
// There is no fundamental theory behind this number.
const tmpStringBufSize = 32
type tmpBuf [tmpStringBufSize]byte
func stringtoslicebyte(buf *tmpBuf, s string) []byte {
var b []byte
if buf != nil && len(s) <= len(buf) {
*buf = tmpBuf{}
b = buf[:len(s)]
} else {
b = rawbyteslice(len(s))
}
copy(b, s)
return b
}
// rawbyteslice allocates a new byte slice. The byte slice is not zeroed.
func rawbyteslice(size int) (b []byte) {
cap := roundupsize(uintptr(size))
p := mallocgc(cap, nil, false)
if cap != uintptr(size) {
memclrNoHeapPointers(add(p, uintptr(size)), cap-uintptr(size))
}
*(*slice)(unsafe.Pointer(&b)) = slice{p, size, int(cap)}
return
}
如果字符串超过32,但是还是希望可以实现0次内存拷贝,应该怎么做?
第一个方案是使用for range
。
func main() {
fmt.Println(testing.AllocsPerRun(1, convert)) // 输出0
}
func convert() {
s := "中国欢迎您,北京欢迎您"
for _, v := range []byte(s) {
_ = v
}
}
第二个方案是使用强转换,基于reflect
和unsafe
func main() {
fmt.Println(testing.AllocsPerRun(1, convert)) // 输出0
}
func convert() {
s := "中国欢迎您,北京欢迎您"
sl := StringToBytes(s)
for _, v := range sl {
_ = v
}
}
func StringToBytes(s string) (b []byte) {
sh := *(*reflect.StringHeader)(unsafe.Pointer(&s))
bh := (*reflect.SliceHeader)(unsafe.Pointer(&b))
bh.Data, bh.Len, bh.Cap = sh.Data, sh.Len, sh.Len
return b
}
性能测试:
var x = "中国欢迎您,北京欢迎您"
// BenchmarkBytesToString 强转换
func BenchmarkBytesToString(b *testing.B) {
for i := 0; i <= b.N; i++ {
_ = StringToBytes(x)
}
}
//BenchmarkBytesToStringNormal 原生
func BenchmarkBytesToStringNormal(b *testing.B) {
for i := 0; i <= b.N; i++ {
_ = []byte(x)
}
}
BenchmarkBytesToString
BenchmarkBytesToString-4 1000000000 0.3269 ns/op
BenchmarkBytesToStringNormal
BenchmarkBytesToStringNormal-4 36955354 34.13 ns/op
很明显,强转换比[]byte(string)
性能要高很多。
字符串拼接
在 Go 语言中,常见的字符串拼接方式有如下 5 种:
使用
+
func plusConcat(n int, str string) string { s := "" for i := 0; i < n; i++ { s += str } return s }
使用
fmt.Sprintf
func sprintfConcat(n int, str string) string { s := "" for i := 0; i < n; i++ { s = fmt.Sprintf("%s%s", s, str) } return s }
使用
strings.Builder
func builderConcat(n int, str string) string { var builder strings.Builder for i := 0; i < n; i++ { builder.WriteString(str) } return builder.String() }
使用
bytes.Buffer
func bufferConcat(n int, s string) string { buf := new(bytes.Buffer) for i := 0; i < n; i++ { buf.WriteString(s) } return buf.String() }
使用
[]byte
func byteConcat(n int, str string) string { buf := make([]byte, 0) for i := 0; i < n; i++ { buf = append(buf, str...) } return string(buf) }
性能测试:
BenchmarkPlusConcat-4 15 107333204 ns/op 530997245 B/op 10018 allocs/op
BenchmarkSprintfConcat-4 7 154160789 ns/op 832915797 B/op 34114 allocs/op
BenchmarkBuilderConcat-4 6637 171604 ns/op 514801 B/op 23 allocs/op
BenchmarkBufferConcat-4 7231 164885 ns/op 368576 B/op 13 allocs/op
BenchmarkByteConcat-4 7371 248181 ns/op 621298 B/op 24 allocs/op
从基准测试的结果来看,strings.Builder > bytes.Buffer > []byte > + > fmt.Sprintf
参考
- https://go.dev/src/runtime/string.go
- https://cs.opensource.google/go/go/+/tls:src/reflect/value.go
- https://geektutu.com/post/hpg-string-concat.html
- https://books.studygolang.com/The-Golang-Standard-Library-by-Example/chapter02/02.1.html
本文由 Chakhsu Lau 创作,采用 知识共享署名4.0 国际许可协议进行许可。
本站文章除注明转载/出处外,均为本站原创或翻译,转载前请务必署名。
2121是