掘金 后端 ( ) • 2024-04-12 09:40

写在文章开头

字符类型是开发中最常用到的类型,不同的语言有着不同的实现,这篇文章我们来聊聊go语言的字符串类型,本文会从go语言底层实现的角度分析字符串的设计与实现,相信读者通过对本文的阅读会对go语言中字符的实现有着不错的理解和掌握。

Hi,我是 sharkChili ,是个不断在硬核技术上作死的 java coder ,是 CSDN的博客专家 ,也是开源项目 Java Guide 的维护者之一,熟悉 Java 也会一点 Go ,偶尔也会在 C源码 边缘徘徊。写过很多有意思的技术博客,也还在研究并输出技术的路上,希望我的文章对你有帮助,非常欢迎你关注我的公众号: 写代码的SharkChili

因为近期收到很多读者的私信,所以也专门创建了一个交流群,感兴趣的读者可以通过上方的公众号获取笔者的联系方式完成好友添加,点击备注 “加群” 即可和笔者和笔者的朋友们进行深入交流。

详解go语言中的字符串

打印不同字符串的长度

我们通过unsafe.Sizeof打印这几个变量类型的长度:

 s := "hello"
 fmt.Println(unsafe.Sizeof(s))

 s1 := "你好"
 fmt.Println(unsafe.Sizeof(s1))
 
 s2 := "你好 sharkChili"
 fmt.Println(unsafe.Sizeof(s2))
 

可以看到任何的字符串打印出来的长度都是16:

16
16
16

go语言字符串底层分析

查看源代码文件string.go,可以看到go语言对于字符串类型的封装,它用一个指针变量str记录字符串的指针,它是由一个指针和一个整型变量len构成的:

type stringStruct struct {
 //指向字符串的指针
 str unsafe.Pointer
 //记录当前字符串的长度
 len int
}

任何指针长度都是8字节,加上在64位的操作系统上,整型默认也是8字节,这也是为什么我们打印的字符串类型长度都是16的原因:

go语言对字符串的优化

所以我们如果需要知道字符串的长度,则需要使用len方法:

func main() {

 s := "hello"
 fmt.Println(len(s))

 s1 := "你好"
 fmt.Println(len(s1))

 s2 := "你好 sharkChili"
 fmt.Println(len(s2))

}

对应的输出结果如下,可以看到通过计算,英文一个1个字母1个字节,而中文一个字符3字节,很明显go语言底层对于不同类型的文字的内存空间做了一定的优化处理:

5
6 
17

我们都知道Unicode是一种几乎可以涵盖世界上所有字符的字符集,它将所有英文字母排在前128位,go语言对于字符串使用Unicode字符集utf8格式的编码格式,由于前128位都是英文字母,所以采用ASCII编码标识,即英文只需1个字节即可存储,而中文在后续的字符集中,所以需要用3个字节来表示。

内置的字符串解码实现字符串安全迭代

对于go语言的迭代的操作,我们可以使用for-range语法进行格式化输出,对应代码示例如下:

func main() {

 s := "你好 sharkChili"
 for _, i := range s {
  fmt.Printf("%c", i)
 }

}

输出结果也正如预期:

你好 sharkChili

因为我们采用utf8编码,所以go语言通过utf8.godecoderuneencoderune确保我们的在使用前得到正确的编码和解码的字符串。 以上我们迭代的例子,在迭代时go语言就会走到decoderune函数,初次迭代时会从字符串数组索引0"你"这个字符串开始,判断该数组位置字符大小,如果在0800-FFFF之间,说明底层要截取3个字节才能正确拿到该元素,decoderune就会截取3字节后得到一个字符(用go的术语叫rune),并将pos加上3确保偏移到下个字节的起始位置:

对应的源码如下:

//传入字符串和当前字符串索引位置,返回字符r和下一个字符的起始位置
func decoderune(s string, k int) (r rune, pos int) {
 pos = k

 if k >= len(s) {
  return runeError, k + 1
 }

//截取当前索引位置后的字符
 s = s[k:]
 //获取截取的第一个位置的字符串的字节数,截取相应字节后计算偏移量直接返回
 switch {
 //2字节字符的截取和偏移计算
 case t2 <= s[0] && s[0] < t3:
  // 0080-07FF two byte sequence
  if len(s) > 1 && (locb <= s[1] && s[1] <= hicb) {
   r = rune(s[0]&mask2)<<6 | rune(s[1]&maskx)
   pos += 2
   if rune1Max < r {
    return
   }
  }
 //3字节字符的截取和偏移计算
 case t3 <= s[0] && s[0] < t4:
  // 0800-FFFF three byte sequence
  if len(s) > 2 && (locb <= s[1] && s[1] <= hicb) && (locb <= s[2] && s[2] <= hicb) {
   r = rune(s[0]&mask3)<<12 | rune(s[1]&maskx)<<6 | rune(s[2]&maskx)
   pos += 3
   if rune2Max < r && !(surrogateMin <= r && r <= surrogateMax) {
    return
   }
  }
 //4字节字符的截取和偏移计算
 case t4 <= s[0] && s[0] < t5:
  // 10000-1FFFFF four byte sequence
  if len(s) > 3 && (locb <= s[1] && s[1] <= hicb) && (locb <= s[2] && s[2] <= hicb) && (locb <= s[3] && s[3] <= hicb) {
   r = rune(s[0]&mask4)<<18 | rune(s[1]&maskx)<<12 | rune(s[2]&maskx)<<6 | rune(s[3]&maskx)
   pos += 4
   if rune3Max < r && r <= maxRune {
    return
   }
  }
 }

 return runeError, k + 1
}

小结

本文通过字符串的底层结构和内存分配及迭代机制上深入剖析了go语言对于字符串类型的设计和实现,希望对你有帮助。

我是 sharkchiliCSDN Java 领域博客专家开源项目—JavaGuide contributor,我想写一些有意思的东西,希望对你有帮助,如果你想实时收到我写的硬核的文章也欢迎你关注我的公众号: 写代码的SharkChili 。 因为近期收到很多读者的私信,所以也专门创建了一个交流群,感兴趣的读者可以通过上方的公众号获取笔者的联系方式完成好友添加,点击备注 “加群” 即可和笔者和笔者的朋友们进行深入交流。

本文使用 markdown.com.cn 排版