Tony Bai · Go 语言第一课
Tony Bai
资深架构师,tonybai.com 博主
21492 人已学习
新⼈⾸单¥59
登录后,你可以任选4讲全文学习
课程目录
已完结/共 59 讲
开篇词 (1讲)
结束语 (1讲)
Tony Bai · Go 语言第一课
15
15
1.0x
00:00/00:00
登录|注册

13|基本数据类型:为什么Go要原生支持字符串类型?

字符视角的迭代
字节视角的迭代
字符串与字节切片、字符串与rune切片的双向转换
字典序的比较策略
strings.Builder、strings.Join、fmt.Sprintf等函数
基于+/+=操作符
for range迭代
常规for迭代
获取字符串中特定下标上的字节
由指向底层存储的指针和字符串的长度字段组成
描述符
支持多种表示法
通过双引号包裹表示多个字符组成的字符串
一个rune实例就是一个Unicode字符
用来表示一个Unicode码点
字符是Unicode字符
字符串是一个可空的字符序列
字节个数称为字符串的长度
字符串是一个可空的字节序列
对非ASCII字符提供原生支持,消除了源码在不同环境下显示乱码的可能
构造多行字符串时的心智负担降低
获取长度的时间复杂度是常数时间
并发安全性和存储利用率提高
字符串转换
字符串比较
字符串连接
字符迭代
下标操作
StringHeader
空间利用率高
兼容ASCII字符内存表示
使用变长度字节,对Unicode字符的码点进行编码
解决Unicode码点值在计算机中如何存储和表示的问题
字符串字面值
rune类型
字符视角
字节视角
好处
Go字符串类型的常见操作
Go字符串类型的内部表示
UTF-8编码方案
原生支持字符串类型的优秀性质
为什么Go要原生支持字符串类型?

该思维导图由 AI 生成,仅供参考

你好,我是 Tony Bai。
在上节课中,我们讲解了在 Go 编程中最广泛使用的一类基本数据类型:数值类型,包括整型、浮点类型和复数类型。这一节课,我们继续来学习 Go 语言中另一类基本数据类型:字符串类型
字符串类型,是现代编程语言中最常用的数据类型之一,多数主流编程语言都提供了对这个类型的原生支持,少数没有提供原生字符串的类型的主流语言(比如 C 语言)也通过其他形式提供了对字符串的支持。
对于这样在日常开发中高频使用的基本数据类型,我们要给予更多的关注。所以,我们这一节课,将会按照 Why-What-How 的逻辑,讲清楚 Go 对字符串类型的支持,让你对 Go 语言中的字符串有个完整而清晰的认识。
首先,让我们来看看为什么 Go 要原生支持字符串类型。

原生支持字符串有什么好处?

我们前面提过,Go 是站在巨人的肩膀上成长起来的现代编程语言。它继承了前辈语言的优点,又改进了前辈语言中的不足。这其中一处就体现在 Go 对字符串类型的原生支持上。
这样的设计会有什么好处呢?作为对比,我们先来看看前辈语言之一的 C 语言对字符串的支持情况。
C 语言没有提供对字符串类型的原生支持,也就是说,C 语言中并没有“字符串”这个数据类型。在 C 语言中,字符串是以字符串字面值或以’\0’结尾的字符类型数组来呈现的,比如下面代码:
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 中文繁体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
    • 阿拉伯语
  • 解释
  • 总结

Go语言原生支持字符串类型,这一设计带来了多重好处。首先,字符串类型的数据是不可变的,提高了字符串的并发安全性和存储利用率。其次,Go语言中字符串没有结尾'\0',获取长度的时间复杂度是常数时间,消除了获取字符串长度的开销。此外,Go语言原生支持“所见即所得”的原始字符串,大大降低构造多行字符串时的心智负担。最后,Go语言对非ASCII字符提供原生支持,消除了源码在不同环境下显示乱码的可能。 文章还介绍了Go字符串的组成,从字节视角和字符序列视角分别进行了解释。通过这些特点,读者可以快速了解Go语言原生支持字符串类型的优势和机制。 此外,文章还介绍了rune类型与字符字面值的关系,以及字符串字面值的构成方式。另外,文章还详细介绍了UTF-8编码方案,以及Go语言中字符串类型的内部表示方式。通过对字符串类型的实现原理的解析,读者可以更好地理解字符串类型的性质和操作。 总的来说,本文通过深入浅出的方式介绍了Go语言中字符串类型的特点、构成和内部表示,为读者提供了全面的了解和认识。文章还介绍了Go字符串的常见操作,包括下标操作、字符迭代、字符串连接、字符串比较和字符串转换。通过这些操作的介绍,读者可以更好地掌握Go语言中字符串类型的使用方法和注意事项。 总的来说,本文通过深入浅出的方式介绍了Go语言中字符串类型的特点、构成和内部表示,为读者提供了全面的了解和认识。

仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《Tony Bai · Go 语言第一课》
新⼈⾸单¥59
立即购买
登录 后留言

全部留言(44)

  • 最新
  • 精选
  • Darren
    func plusConcat(n int, str string) string { // +号拼接 } func sprintfConcat(n int, str string) string { //fmt.Sprintf拼接 } func builderConcat(n int, str string) string { var builder strings.Builder for i := 0; i < n; i++ { builder.WriteString(str) } return builder.String() } func bufferConcat(n int, s string) string { buf := new(bytes.Buffer) for i := 0; i < n; i++ { buf.WriteString(s) } return buf.String() } func byteConcat(n int, str string) string { buf := make([]byte, 0) for i := 0; i < n; i++ { buf = append(buf, str...) } return string(buf) } func preByteConcat(n int, str string) string { buf := make([]byte, 0, n*len(str)) for i := 0; i < n; i++ { buf = append(buf, str...) } return string(buf) } func builderGrowConcat(n int, str string) string { var builder strings.Builder builder.Grow(n * len(str)) // 与builderConcat相同 } func bufferGrowConcat(n int, s string) string { buf := new(bytes.Buffer) buf.Grow(n * len(s)) // 与bufferConcat相同 } benchmem测试: 24 47124538 ns/op 530996721 B/op 10011 allocs/op 13 81526461 ns/op 834307836 B/op 37463 allocs/op 13263 90613 ns/op 505841 B/op 24 allocs/op 12730 94213 ns/op 423537 B/op 13 allocs/op 12992 94185 ns/op 612338 B/op 25 allocs/op 23606 50058 ns/op 212992 B/op 2 allocs/op 24326 49660 ns/op 106496 B/op 1 allocs/op 16762 71860 ns/op 212993 B/op 2 allocs/op 如果能知道拼接字符串的个数,那么使用bytes.Buffer和strings.Builder的Grows申请空间后,性能是最好的;如果不能确定长度,那么bytes.Buffer和strings.Builder也比“+”和fmt.Sprintf性能好很多。 bytes.Buffer与strings.Builder,strings.Builder更合适,因为bytes.Buffer 转化为字符串时重新申请了一块空间,存放生成的字符串变量,而 strings.Builder 直接将底层的 []byte 转换成了字符串类型返回了回来。 bytes.Buffer 的注释中还特意提到了: To build strings more efficiently, see the strings.Builder type.

    作者回复: 简直就是标准答案👍

    2021-11-11
    2
    145
  • Vfeelit
    rune 是 int32 别名 Unicode编码没有负的吧 为何不是 uint32的别名?

    作者回复: 好问题。这个问题我也曾想过,官方没有答案。但从社区给出的观点来看,主要考虑两点:1.int32足够表示unicode所有码点 2. int32可以为负数,便于检测溢出(overflow)或其他基于int32的计算错误。

    2022-01-09
    23
  • 大大大大大泽
    有个问题不太懂。。。UTF-8 编码使用的字节数量从 1 个到 4 个不等。那么如何确定几个字节确定一个字符呢? 比如说 中国人 是 \xe4\xb8\xad\xe5\x9b\xbd\xe4\xba\xba,3个字节确定一个字符,分配为3 3 3。为什么不会分割成1 1 2 2 3

    作者回复: 摘自网络:) unicode 符号范围 | utf-8 编码方式 00000000 ~ 0000007F | 0xxxxxxx 00000080 ~ 000007FF | 110xxxxx 10xxxxxx 00000800 ~ 0000FFFF | 1110xxxx 10xxxxxx 10xxxxxx 00010000 ~ 0010FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx 总结下来,针对UTF8,编码规则其实只有两条: 1)单字节规则: 对于 单字节 的符号,字节的第一位(最高位)设为 0,后面 7 位为这个符号的 unicode 码。 2)n字节规则: 对于 n 字节的符号(n>1),第一个字节的前 n 位都设为 1,第 n+1 位设为 0,后面字节的前两位一律设为 10。剩下的没有提及的二进制位,全部为这个符号的 unicode 码。

    2022-03-01
    3
    17
  • 你好呀, 朋友.
    是不是可以理解成[]rune里存的是Unicode码点或者说UTF-32编码,而[]byte和string存的是UTF-8编码

    作者回复: 一个rune存储一个unicode码点或utf-32的四字节编码;从字节视角,string对应的底层存储存放的是utf8编码。

    2021-11-12
    12
  • qinsi
    那么问题来了,raw string里要怎么使用反引号?

    作者回复: 反引号是唯一的“漏网之鱼”:)。

    2021-11-11
    2
    12
  • William Ning
    现代CPU计算时一次都能装载多个字节(如32位计算机一次装载4字节),多字节的数值在内存中高低位的排列方式会影响所表示的数值,以数值0x01020304为例,在内存中用4个字节存储,4个字节的内容分别是0x01、0x02、0x03、0x04。根据字节高低位排序方式的不同,可以分为:大端字节序(big endian)和小端字节序(little endian)。 大端字节序 大端字节序是指一个整数的高位字节(如上例中的0x01)存储在内存的低地址处,高字节在前。 C语言数组存储例: 0x01020304 bufe[0] = 0x01; bufe[1] = 0x02; bufe[2] = 0x03; bufe[3] = 0x04; 小端字节序 小端字节序把数值的低位字节(如上例中的0x04)存储在内存的低地址处,低字节在前。PC计算机和单片机常见都是小端字节序。 C语言数组存储例: 0x01020304 bufe[0] = 0x04; bufe[1] = 0x03; bufe[2] = 0x02; bufe[3] = 0x01; 常见的memcpy函数复制float字节到数组中,数组中的float就是小端字节序 memcpy(&listDataSoft[0] ,&f,sizeof(float)); 主机字节序 现代计算机大多采用小端字节序,所以小端字节序又叫主机字节序。 网络字节序 不同的计算机可能会采用不同的字节序,甚至同一计算机上不同进程会采用不同的字节序,如JAVA虚拟机采用大端字节序,可能和采用小端字节序计算机上的其他进程不同。所以在网络通信(或进程间通信)时,如果都按自己存储的顺序收发数据,有可能会出现一些误解,为了避免这个问题,约定数据在不同计算机之间传递时都采用大端字节序,也叫作网络字节序。通信时,发送方需要把数据转换成网络字节序(大端字节序)之后再发送,接收方再把网络字节序转成自己的字节序。

    作者回复: 👍

    2022-03-01
    10
  • 布凡
    strings.Builder的效率要比+/+=的效率高 因为string.Builder 是先将第一个字符串的地址取出来,然后将builder的字符串拼接到后面, func (b *Builder) copyCheck() { if b.addr == nil { // This hack works around a failing of Go's escape analysis // that was causing b to escape and be heap allocated. // See issue 23382. // TODO: once issue 7921 is fixed, this should be reverted to // just "b.addr = b". b.addr = (*Builder)(noescape(unsafe.Pointer(b))) } else if b.addr != b { panic("strings: illegal use of non-zero Builder copied by value") } } // String returns the accumulated string. func (b *Builder) String() string { return *(*string)(unsafe.Pointer(&b.buf)) } +/+=是将两个字符串连接后分配一个新的空间,当连接字符串的数量少时,两者没有什么区别,但是当连接字符串多时,Builder的效率要比+/+=的效率高很多。如有理解不正确的地方希望老师同学指正!(*^_^*)

    作者回复: 点个赞。

    2021-11-10
    8
  • 多选参数
    老师讲编码是我见过讲的最清晰的。有个小问题,就是 Go 中的 string 在内存中存的应该还是 UTF-8 编码之后的数据?而 rune 的方式是在我们使用的时候 Go 源码隐式的进行了转换?

    作者回复: 没错,string 在内存中存的就是utf8编码后的字节。像for range这种循环得到的rune,是Go编译器在编译时做的替换。

    2021-11-20
    7
  • 多选参数
    老师,关于 utf-8 不考虑字节序的问题。能否这么理解,utf-8 的一个字符是由 3 个字节逐个字节进行编码比较决定的,比如第一个字节编码的值在这个值之间,那肯定采用的是单字节编码,第二个字节编码的值在这之间,那肯定是双字节编码,而 utf-32 需要 4 字节一起考虑?那么,一旦 4 个字节一起考虑了的话,就需要涉及到这 4 个字节是大端序还是小端序?

    作者回复: 对的。

    2021-11-20
    6
  • Bynow
    & 和 unsafe.Pointer 有什么区别?

    作者回复: 以a:=1; var p = &a为例,&是取地址操作符。unsafe.Pointer是go语言中的通用指针类型,任何指针都可以转型为unsafe.Pointer类型,反之unsafe.Pointer也可以转回任意指针类型。例子: i := 11 var p = unsafe.Pointer(&i) // int指针 -> unsafe.Pointer pi := (*int)(p) // unsafe.Pointer -> int指针

    2021-11-25
    5
收起评论
显示
设置
留言
44
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部