13|基本数据类型:为什么Go要原生支持字符串类型?
13|基本数据类型:为什么Go要原生支持字符串类型?
讲述:Tony Bai
时长24:21大小22.24M
原生支持字符串有什么好处?
Go 字符串的组成
rune 类型与字符字面值
字符串字面值
UTF-8 编码方案
Go 字符串类型的内部表示
Go 字符串类型的常见操作
小结
思考题
赞 50
提建议
精选留言(42)
- Darren2021-11-11func 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.展开
作者回复: 简直就是标准答案👍
111 - Aaron Liu2021-11-10这是见过讲string最详细的,原理,实操,对比,背后的设计以及常用方法,很透彻,继续学习28
- Vfeelit2022-01-09rune 是 int32 别名 Unicode编码没有负的吧 为何不是 uint32的别名?
作者回复: 好问题。这个问题我也曾想过,官方没有答案。但从社区给出的观点来看,主要考虑两点:1.int32足够表示unicode所有码点 2. int32可以为负数,便于检测溢出(overflow)或其他基于int32的计算错误。
13 - 大大大大大泽2022-03-01有个问题不太懂。。。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 码。
共 2 条评论11 - qinsi2021-11-11那么问题来了,raw string里要怎么使用反引号?
作者回复: 反引号是唯一的“漏网之鱼”:)。
共 2 条评论8 - 你好呀, 朋友.2021-11-12是不是可以理解成[]rune里存的是Unicode码点或者说UTF-32编码,而[]byte和string存的是UTF-8编码
作者回复: 一个rune存储一个unicode码点或utf-32的四字节编码;从字节视角,string对应的底层存储存放的是utf8编码。
7 - 布凡2021-11-10strings.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的效率要比+/+=的效率高很多。如有理解不正确的地方希望老师同学指正!(*^_^*)展开
作者回复: 点个赞。
6 - 多选参数2021-11-20老师,关于 utf-8 不考虑字节序的问题。能否这么理解,utf-8 的一个字符是由 3 个字节逐个字节进行编码比较决定的,比如第一个字节编码的值在这个值之间,那肯定采用的是单字节编码,第二个字节编码的值在这之间,那肯定是双字节编码,而 utf-32 需要 4 字节一起考虑?那么,一旦 4 个字节一起考虑了的话,就需要涉及到这 4 个字节是大端序还是小端序?
作者回复: 对的。
4 - William Ning2022-03-01现代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虚拟机采用大端字节序,可能和采用小端字节序计算机上的其他进程不同。所以在网络通信(或进程间通信)时,如果都按自己存储的顺序收发数据,有可能会出现一些误解,为了避免这个问题,约定数据在不同计算机之间传递时都采用大端字节序,也叫作网络字节序。通信时,发送方需要把数据转换成网络字节序(大端字节序)之后再发送,接收方再把网络字节序转成自己的字节序。展开
作者回复: 👍
4 - 多选参数2021-11-20老师讲编码是我见过讲的最清晰的。有个小问题,就是 Go 中的 string 在内存中存的应该还是 UTF-8 编码之后的数据?而 rune 的方式是在我们使用的时候 Go 源码隐式的进行了转换?
作者回复: 没错,string 在内存中存的就是utf8编码后的字节。像for range这种循环得到的rune,是Go编译器在编译时做的替换。
3 - lesserror2021-11-11Tony Bai 老师的这篇关于Go字符串类型的讲解非常细致。 但还是有以下困惑,麻烦老师看到了解答一下: 1. 怎么理解:“字面值” 比较贴切? 2. unsafe.Pointer这个用法,这个在源代码中挺常见的,本专栏会有讲解吗? 3. var s string = "中国人",像这种变量声明,最佳实践是删去掉string的类型声明吗?我这边编辑器直接提示我是多余的声明。 4. 关于string 类型的数据是不可变的,由此带来的种种好处,感觉还可以深入讲讲,这里感觉还是比较抽象。展开
作者回复: 1. 字面值(literal)就是源码中的一个固定的值,它直接写在源码中,不可变,且不需经过任何计算我们就能从字面上看出其“值”。在编程语>言中,通常一个字面值都可以从其字面上大致推断出其类型。另外字面值可以用于初始化变量,也可以作为常量的值。 2. unsafe包属于高级话题,本专栏作为入门专栏后续也不会太过深入。 3. 嗯,用s:="中国人",估计编辑器就不会提示了 4. 好建议!
3 - Elvis Lee2022-01-06string是一个8位字节的集合,通常但不一定代表UTF-8编码的文本。string可以为空,但是不能为nil。string的值是不能改变的。 string类型虽然是不能更改的,但是可以被替换,因为stringStruct中的str指针是可以改变的,只是指针指向的内容是不可以改变的,也就说每一个更改字符串,就需要重新分配一次内存,之前分配的空间会被gc回收。
作者回复: 👍
2 - Bynow2021-11-25& 和 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指针
2 - 在下宝龙、2021-11-10老师您好,一个中文字 在utf-8编码之后是三个字节 ,那为什么会没有字节序问题,我有点弄不明白
作者回复: utf8是变长编码,其编码单元是单个字节,不存在谁在高位、谁在低位的问题。而utf-16的编码单元是双字节,utf-32编码单元为4字节,均需要考虑字节序问题。
共 6 条评论2 - 6010738912022-05-07白老师您好, 我这里看到这里的“获取长度的时间复杂度是常数时间”,这样按理来说如果需要判断字符串s是否为空,最好是用len(s) == 0,而不是用s == ""来判断,因为这样是直接判断string所映射的二元数组的len的内容,而不需要一步步的对比data的内容,但我按网上找的一个测试用例来进行性能比较,发现基本是没有区别的: package kong_test import ( "testing" ) func isEmptyString0() bool { var data string if data == "" { return true } return false } func isEmptyString1() bool { var data string if len(data) == 0 { return true } return false } func BenchmarkIsEmptyString0(b *testing.B) { for i := 0; i < b.N; i++ { isEmptyString0() } } func BenchmarkIsEmptyString1(b *testing.B) { for i := 0; i < b.N; i++ { isEmptyString1() } } ================测试结果如下: [root@VM-24-4-centos test0]# go test -bench=. -v -benchmem goos: linux goarch: amd64 pkg: example/test0 cpu: Intel(R) Xeon(R) Platinum 8255C CPU @ 2.50GHz BenchmarkIsEmptyString0 BenchmarkIsEmptyString0-2 1000000000 0.3359 ns/op 0 B/op 0 allocs/op BenchmarkIsEmptyString1 BenchmarkIsEmptyString1-2 1000000000 0.3312 ns/op 0 B/op 0 allocs/op PASS ok example/test0 0.748s ==================== 所以想请问一下白老师这是什么原因导致的呢?还是说其实用len(s) == 0的方法判断和用s == ""来判断其实是没有区别的呢?k8s源码里大量使用的len(s) == 0而不是s == ""只是个人习惯或者巧合吗? 感谢!展开
作者回复: 好问题。 len(s)== 0实质是一个int比较,而s == ""实质也是一个byte比较,两者没啥差别。string比较是逐个byte比较的。之前写过一篇文章 :https://tonybai.com/2022/04/18/inside-go-string-comparison 可以看看。
1 - Geek_99b47c2021-11-16“了解了 string 类型的实现原理后,我们还可以得到这样一个结论,那就是我们直接将 string 类型通过函数 / 方法参数传入也不会带来太多的开销。因为传入的仅仅是一个“描述符”,而不是真正的字符串数据。” func main() { var a = "chsir" fmt.Printf("main:%p\n", &a) hello(&a) } func hello(a *string) { fmt.Printf("hello:%p\n", &a) } 请教一下,传入的“描述符”,为什么在main函数和hello函数打印的地址不一样啊,这样要怎么理解,是go复制了一份“描述符”吗?展开
作者回复: 复制的是“描述符”的值,不是描述符变量自身的地址。
共 5 条评论1 - 羊羊2021-11-10var s string = "hello" s[0] = 'k' s = "gopher" 根据后面的内容知道s[0]是字符串s的第一个字节,不是第一个字符,是不是不能把字符k直接赋值给s[0]呢?go的字符串是不是,不能用index来获取其中的单个字符?
作者回复: s[0] = 'k' 这个肯定不可以。go string是不可更改的。 go字符串下标操作只能获得第一个字节,而不是第一个字符。
1 - 罗杰2021-11-10关于拼接性能,有没有权威的文章介绍,我对比测试发现的结果 fmt.Sprintf 基本都是最差的
作者回复: 这个没啥权威文章,自己写benchmark跑一下就大体能判断出那个性能更好了。
1 - 子杨2022-12-14 来自辽宁小小的字符串,东西真不少
作者回复: 👍
- Sunrise2022-11-16 来自北京s1 := "世界和平" s2 := "世界" s3 := "和平" s4 := s2 + s3 请问s1, s2, s3, s4 是共用的一块底层数据“世界和平”吗?是否和数组和切片一个原理,s2, s3,s4 是 s1 的切片展开
作者回复: 好问题。 实际测试并不是。 共享底层string这个技术叫string interning。关于go对string interning的支持可以参见https://github.com/golang/go/issues/5160 目前多数情况下,interning依旧无法得到支持。