Go 字符串的常见操作

105 阅读3分钟

「这是我参与2022首次更文挑战的第7天,活动详情查看:2022首次更文挑战」。

背景

由于字符串的不可变性,针对字符串,更多是尝试对其进行读取,或者将它作为一个组成单元去构建其他字符串,又或是转换为其他类型

下标操作

  • 在字符串的实现中,真正存储数据的是底层的数组
  • 字符串的下标操作本质上等价于底层数组的下标操作
package main

import (
	"fmt"
)

func main() {
	var s = "中国人"
	fmt.Printf("0x%x\n", s[0]) // 0xe4:字符“中” utf-8编码的第一个字节
}

运行结果

0xe4

通过下标操作,获取的特定下标上的字节,而不是字符

字符迭代

有两种迭代形式

  1. 常规 for 迭代
  2. for range 迭代

通过这两种形式的迭代对字符串进行操作得到的结果是不同的\

常规 for 迭代

是一种字节视角的迭代,每轮迭代得到的结果都是组成字符串内容的一个字节,以及字节所在的下标值,这也等价于对字符串底层数组的迭代

package main

import (
	"fmt"
)

func main() {
	var s = "中国人"

	for i := 0; i < len(s); i++ {
		fmt.Printf("index: %d, value: 0x%x\n", i, s[i])
	}
}

运行结果

index: 0, value: 0xe4
index: 1, value: 0xb8
index: 2, value: 0xad
index: 3, value: 0xe5
index: 4, value: 0x9b
index: 5, value: 0xbd
index: 6, value: 0xe4
index: 7, value: 0xba
index: 8, value: 0xba

获取到的是字符串里字符的 UTF-8 编码中的一个字节

for range 迭代

是一种字符视角

package main

import (
	"fmt"
)

func main() {
	var s = "中国人"

	for i, v := range s {
		fmt.Printf("index: %d, value: 0x%x\n", i, v)
	}
}

运行结果

index: 0, value: 0x4e2d
index: 3, value: 0x56fd
index: 6, value: 0x4eba

得到的是字符串中 Unicode 字符的码点值,以及该字符在字符串中的偏移值

len(字符串)

获取的是字符串内容的字节个数

如果想获取字符串中的字符个数

应该使用标准库 UTF-8 包中的 RuneCountInString 函数

package main

import (
	"fmt"
	"unicode/utf8"
)

func main() {
	var s = "中国人"
	fmt.Println("the char count in s is: ", utf8.RuneCountInString(s))
}

运行结果

the char count in s is:  3

3 个中文,就是 3 个字符啦

字符串连接

Go 原生支持通过 +/+= 操作符进行字符串连接

package main

import (
	"fmt"
)

func main() {
	s := "i am "
	s = s + "poloyy "
	s += "tester"

	fmt.Println(s)
}

运行结果

i am poloyy tester
  • +/+= 进行字符串连接的开发体验是最好的,但却不是性能最好的方式
  • Go 还提供了 strings.Builder、strings.Join、fmt.Sprintf 等函数来进行字符串连接操作

字符串比较

  • Go 字符串类型支持各种比较关系操作符,包括 ==、!= 、>=、<=、>、<
  • 从字符串的起始处,开始逐个字节地对两个字符串变量进行比较
  • 当两个字符串之间出现了第一个不相同的元素,比较就结束了,这两个元素的比较结果就会做为串最终的比较结果
  • 如果出现两个字符串长度不同的情况,长度比较小的字符串会用空元素补齐,空元素比其他非空元素都小
package main

import (
	"fmt"
)

func main() {
	// ==
	s1 := "世界和平"
	s2 := "世界" + "和平"
	fmt.Println(s1 == s2)

	// !=
	s1 = "Go"
	s2 = "C"
	fmt.Println(s1 != s2)

	// > and >=
	s1 = "12345"
	s2 = "23456"
	fmt.Println(s1 < s2)
	fmt.Println(s1 <= s2)

	// > and >=
	s1 = "12345"
	s2 = "123"
	fmt.Println(s1 > s2)
	fmt.Println(s1 >= s2)
}

运行结果

true
true
true
true
true
true
  • 如果两个字符串长度相同,就要进一步判断,数据指针是否指向同一块底层存储数据
  • 如果还相同,那么我们可以说两个字符串是等价的,如果不同,那就还需要进一步去比对实际的数据内容

字符串转换

Go 支持字符串与字节切片、字符串与 rune 切片的双向转换,并且这种转换无需调用任何函数,只需使用显式类型转换就可以了

package main

import (
	"fmt"
)

func main() {
	var s = "小菠萝测试笔记"
	// string -> []rune
	rs := []rune(s)
	fmt.Printf("%x\n", rs)

	// string -> []byte
	bs := []byte(s)
	fmt.Printf("%x\n", bs)

	// []rune -> string
	s1 := string(rs)
	fmt.Println(s1)

	// []byte -> string
	s2 := string(bs)
	fmt.Println(s2)
}

运行结果

[5c0f 83e0 841d 6d4b 8bd5 7b14 8bb0]
e5b08fe88fa0e8909de6b58be8af95e7ac94e8aeb0
小菠萝测试笔记
小菠萝测试笔记
  • 无论是 string 转切片,还是切片转 string,这类转型背后也是有着一定开销的
  • 这些开销的根源就在于 string 是不可变的,运行时要为转换后的类型分配新内存