本文将讲解Go中字符串相关的知识。
1 编码知识
在讲解String之前,我们先讲解一下编码。因为在讲解string过程中,会用到编码知识。
1.1 字符集
字符集规定了某个文字对应的二进制数字存放方式(编码)以及某串二进制数字代表了哪个文字(解码)的转换关系。比如我们常见的Unicode字符集。相当于定义了一套标准。
1.2 编码
编码字符集:用一个编码值(code point)来表示一个字符在字库中的位置。
字符编码:编码字符集和实际存储数据之间的转换关系。比如我们常见的utf-8编码。相当于标准的一个实现。
1.3 乱码
我们在开发中,经常遇到的一个棘手的问题就是乱码。为什么乱码呢?就是因为编、解码采用的字符编码不一致。就好比同样的一串字符,在英语跟俄语中的含义可能不一样。
2 string的结构
type StringHeader struct {
Data uintptr
Len int
}
是不是跟slice很像?
就是一个类型,加一个长度。比slice少了一个cap的定义。这是因为string是一个不可变的,对原string的任何操作操作,都会产生一个新的string。
3 长度
这部分我们看一下字符串的长度问题。
举个例子:
func main() {
s := "hello 中国"
fmt.Println(len(s)) // 12
}
结论是12,而不是8。为什么呢?
// The len built-in function returns the length of v, according to its type:
func len(v Type) int
这是因为len统计string的长度,是按照字节进行统计的。string在Go中默认采用Unicode编码,一个汉字占3个字节,所以就是12.
如果我们想得到8怎么办呢?这儿就需要使用rune了。
先来个例子,再讲原理:
func main() {
str := "hello 中国"
fmt.Println(len(str)) // 12
str2 := []rune(str)
fmt.Println(len(str2)) //8
}
这是为什么呢?先看下rune的定义:
// rune is an alias for int32 and is equivalent to int32 in all ways. It is
// used, by convention, to distinguish character values from integer values.
type rune rune
rune是int32的别名,可以存放4个字节,所以汉字虽然占了3个字节,但是也只是占用一个rune。
4 循环
4.1 经典循环 for i :=0; i< len(str); i++
先来个例子:
str := "hello 中国"
for i := 0; i < len(str); i++ {
fmt.Println(i, str[i], string(str[i]))
}
结论:
0 104 h
1 101 e
2 108 l
3 108 l
4 111 o
5 32
6 228 ä
7 184 ¸
8 173
9 229 å
10 155 �
11 189 ½
发现有乱码,因为如上文所讲,汉字占3个字节,如果按照字节遍历字符串,读到汉字时,每次读取一个字节,就出现乱码呢。
那怎么办呢?看下面一节。
4.2 for range
先对上面的case进行一下改造:
str := "hello 中国"
for k, v := range str {
fmt.Println(k, v, string(v))
}
结果呢?
0 104 h
1 101 e
2 108 l
3 108 l
4 111 o
5 32
6 20013 中
9 22269 国
这是为什么呢?
如之前我的文章 Go-for range 一文 所讲,for range 在遍历string 时,是按照rune进行处理的。
原理可以参考我的文章。
5 类型转换 rune-string-[]byte
在讲解具体的转换之前,我们必须强调一下:无论哪种类型转换到另外一种,都需要内存拷贝,这是会损耗性能的
。
5.1 rune与[]byte转换
下面给一个例子,例子中三个方法,每一个都是基于前一个的优化:
func TestRune2String2Byte_func1(t *testing.T) {
fmt.Println("最简单,但是性能差,因为涉及rune 2 string ,然后 string 2 byte, 两次内存拷贝,所以性能差")
rs := []rune{'H', 'e', 'l', 'l', 'o', ' ', '世', '界'}
bs := []byte(string(rs))
fmt.Printf("%s\n", bs)
fmt.Println(string(bs))
}
func TestRune2String2Byte_func2(t *testing.T) {
fmt.Println("不涉及string的两次转换,但是bs 分配到内存太大")
rs := []rune{'H', 'e', 'l', 'l', 'o', ' ', '世', '界'}
bs := make([]byte, len(rs)*utf8.UTFMax)
count := 0
for _, r := range rs {
count += utf8.EncodeRune(bs[count:], r)
}
bs = bs[:count]
fmt.Printf("%s\n", bs)
fmt.Println(string(bs))
}
func TestRune2String2Byte_func3(t *testing.T) {
fmt.Println("先统计rune的大小,然后分配指定大小的slice。 接着才拷贝到slice中")
rs := []rune{'H', 'e', 'l', 'l', 'o', ' ', '世', '界'}
size := 0
for _, r := range rs {
size += utf8.RuneLen(r)
}
bs := make([]byte, size)
count := 0
for _, r := range rs {
count += utf8.EncodeRune(bs[count:], r)
}
fmt.Printf("%s\n", bs)
fmt.Println(string(bs))
}
方法一(TestRune2String2Byte_func1):很简单地就可以将rune转换成byte,但是中间涉及到两次内存拷贝,所以性能差一些。
方法二(TestRune2String2Byte_func2):直接将rune转换成byte[],但是byte[]的内存占用太大;
方法三(TestRune2String2Byte_func3):在方法二的基础上进行了优化,对byte[]的大小进行了限制,根据实际占用分配。
5.2 string与rune 、string与[]byte转换
下面是string 与 rune, string 与[]byte 转换的例子:
str := "hello go gogo"
//string 转[]byte
b := []byte(str)
//[]byte转string
str = string(b)
//string 转 rune
r := []rune(str)
//rune 转 string
str = string(r)
6 常见API举例
这部分罗列了string常见的一些用法,其内部实现还是比较经典的,等有时间需要拜读一下内部实现:
func TestStringApi(t *testing.T) {
s := "Hello string sasasasaS"
// 以某个字符开头
strings.HasPrefix(s, "test")
// 以某个字符结尾
strings.HasSuffix(s, "test")
// TODO e 在s中第一次出现的位置, 没有则 -1
strings.Index(s, "e")
// TODO e 在s中最后出现的位置,如果没有,返回-1
strings.LastIndex(s, "e")
// 字符串替换
oldStr := "s"
newStr := "a"
// n 为最多替换几个
fmt.Println(strings.Replace(s, oldStr, newStr, 2)) // Hello atring aasasasaS
// 字符串计数
fmt.Println(strings.Count(s, "sa")) // 4
// 重复count次str
fmt.Println(strings.Repeat(s, 2)) // Hello string sasasasaSHello string sasasasaS
// 转为小写
fmt.Println(strings.ToLower(s)) // hello string sasasasas
// 转为大写
fmt.Println(strings.ToUpper(s)) // HELLO STRING SASASASAS
// 去掉字符串收尾空白字符
s1 := " Hello string sasasasaS "
fmt.Println("原字符串长度: ", len(s1)) // 原字符串长度: 24
fmt.Println("去掉收尾空白字符长度", len(strings.TrimSpace(s1))) // 去掉收尾空白字符长度 22
// 去掉字符串首尾cut字符
s2 := "SHello string sasasasaS"
fmt.Println(strings.Trim(s2, "S")) // Hello string sasasasa
fmt.Println(strings.TrimLeft(s2, "S")) // Hello string sasasasaS
fmt.Println(strings.TrimRight(s2, "S")) // SHello string sasasasa
// 返回字符串 空格分隔的所有子串的slice
s3 := "SHello string sasasasaS"
fmt.Println(strings.Fields(s3)) // [SHello string sasasasaS]
// 返回str split 分隔的所有子串的slice
s4 := "SHello string sasasasaS"
fmt.Println(strings.Split(s4, "s")) // [SHello tring a a a aS]
// 用sep把s1中的所有元素链接起来
sArr := []string{"zp", "chris", "lmm"}
fmt.Println(strings.Join(sArr, "---")) // zp---chris---lmm
// 把一个整数转换为字符串
i := 1
fmt.Println(strconv.Itoa(i))
// 把字符串转换为整数
strTest := "a"
fmt.Println(strconv.Atoi(strTest)) // 0 strconv.Atoi: parsing "a": invalid syntax
fmt.Println(strconv.Atoi("1")) // 1 <nil>
}
7 总结
本文从编码入手,讲解了string的结构、长度、循环,以及各种类型之间的转换。最后罗列了string常见的api。
8 参考文献
十分钟搞清字符集和字符编码 http://cenalulu.github.io/linux/character-encoding/
Golang rune []byte string 的相互转换https://blog.csdn.net/dengming0922/article/details/80883574
【golang】浅析rune,byte https://blog.csdn.net/HaoDaWang/article/details/79971395
9 其他
本文是《循序渐进go语言》的第十二篇-《Go-string》。
如果有疑问,可以直接留言,也可以关注公众号 “链人成长chainerup” 提问留言,或者加入知识星球“链人成长” 与我深度链接~
有疑问加站长微信联系(非本文作者)