字符串string

string 是Go语言中的基础数据类型。

特性速览

声明

声明string变量非常简单,常见的方式有以下两种:

声明一个空字符串后再赋值

var s string
s = "hello world"

需要注意的是空字符只是长度为0,但不是nil。不存在值为nil的string

使用简短变量声明:

s := "hello world"    //直接初始化字符串

双引号与单引号

字符串不仅可以使用双引号赋值,也可以使用反单引号赋值,它们的区别是在于对特殊字符的处理。

假如我们希望string变量表示下面的字符串,它包括换行符和双引号:

Hi,
this is "Steven".

使用双引号表示时,需要对特殊字符转义,如下所示:

s:= "Hi, \nthis is \"Steven\"."

如果使用反单引号时,不需要对特殊符号转义,如下所示:

s := `Hi,
this is "Steven".`

使用反单引号表示字符串比较直观,可以清晰的看出字符串内容。如果有数据库sql以及描述性说明,可以优先使用反单引号的方式表达。

字符串拼接

字符串可以使用加号进行拼接:

s = s + "a" + "b"

需要注意的是,字符串拼接会触发内存分配以及内存拷贝,单行语句拼接多个字符串只分配一次内存。比如上面的语句中,在拼接时,会先计算最终字符串的长度后再分配内存。

类型转换

项目中,数据经常需要在string和字节[]byte之间转换

[]byte 转 string

func ByteToString(){b:=[]byte{'h','e','l','l','o'}s:=string(b)fmt.Println(s) //hello
}

string 转 []byte

func StringToByte(){s := "hello"b := []byte(s)fmt.Println(b)
}

需要注意的是,无论是字符串转成[]byte,还是[]byte转成string,都将发生一次内存拷贝,会有一定的性能开销。

正因为string和[]byte之间的转换非常方便,在某些高频场景中往往会成为性能的瓶颈,比如数据库访问、http请求处理等。

特点

UTF编码

string使用8比特字节的集合来存储字符,而且存储的字符是UTF-8编码。

在使用for-range 遍历字符串时,每次迭代将返回UTF-8编码的首个字节下标以及字节值,这意味着下标可能不连续。

比如下面的函数:

func StringIteration(){s :="中国"for i,v :=range s {fmt.Printf("i : %d , v : %c \n ",i , v)}
}

函数输出:

i : 0 , v : 中
i : 3 , v : 国

此外,字符串的长度是指字节数,而非字符数。 比如汉字"中"和"国"的UTF-8编码各占3个字节,字符串"中国"的长度是6而不是2。

不可变

字符串可以为空,但值不会是nil。另外字符串不可以修改(和Java语言中的String一样)。字符串变量可以接受新的字符串赋值,但是不能通过下标的方式进行修改字符串的值。

如下所示:

s := "Hello"
&s[0]=byte(104)    //非法
s = "hello"  //合法

字符串不支持取地址操作,也就无法修改字符串的值,上面的语句中会出现编译错误:

cannot take the address of s[0]

标准函数库

标准库strings包提供了大量的字符串操作函数。可以参考 [Go语言中文网] ,如下所示:

实现原理

Go标准库builtin中定义了string类型:

type string string
8位byte序列构成的字符串,约定但不必须是utf-8编码的文本。字符串可以为空但不能是nil,其值不可变。

数据结构

源码包中 src/runtime/string.go:stringStruct 定义了string的数据结构:

type stringStruct struct {str unsafe.Pointerlen int
}

string的数据结构很简单,只包含两个成员。

  • str :字符串的首地址
  • len:字符串的长度

string的数据结构跟切片类似,只不过切片slice还有一个表示容量的变量,事实上string和切片slice([]byte)经常转换。

在runtime包中使用gostringnocopy()函数来生成字符串。如下代码所示,声明一个string变量并赋值:

var str string
str = "Hello World"

字符串生成时,会先构建stringStruct对象,再转成string。转换的源码如下:

//go:nosplit
func gostringnocopy(str *byte) string {//先构造stringStructss := stringStruct{str: unsafe.Pointer(str), len: findnull(str)}//再将stringStruct转成strings := *(*string)(unsafe.Pointer(&ss))return s
}

string在runtime包中是stringStruct类型,对外呈现则为string类型。

字符串表示

字符串使用Unicode编码存储字节,对于英文字符来说,每个字符的Unicode编码只用一个字节即可表示,如下图所示:

此时字符串的长度等于字符数。而对于非ASCII字符来说,其Unicode编码可能需要更多的字节来表示,如下图所示:

此时字符串的长度会大于实际字符数,字符串的长度实际上表现的是字节数。

字符串拼接

字符串可以很方便的拼接,像下面所示:

str :="str1" + "str2" + "str3"

即便有非常多的字符串需要拼接,性能上也有比较好的保证,因为新的字符串内存空间是一次性分配完成的,所以性能主要是消耗在内存拷贝上。

在runtime包中,使用concatstrings()函数来拼接字符串。在一个拼接语句中,所有待拼接字符串都被编译器组织到一个切片中并传入concatstrings()函数中,拼接过程需要遍历两次切片,第一次遍历获取总的字符串长度,据此来申请内存空间,第二次遍历会将字符串逐个拷贝进去。

// concatstrings implements a Go string concatenation x+y+z+...
// The operands are passed in the slice a.
// If buf != nil, the compiler has determined that the result does not
// escape the calling function, so the string data can be stored in buf
// if small enough.
func concatstrings(buf *tmpBuf, a []string) string {idx := 0l := 0count := 0for i, x := range a {n := len(x)if n == 0 {continue}if l+n < l {throw("string concatenation too long")}l += ncount++idx = i}if count == 0 {return ""}// If there is just one string and either it is not on the stack// or our result does not escape the calling frame (buf != nil),// then we can return that string directly.if count == 1 && (buf != nil || !stringDataOnStack(a[idx])) {return a[idx]}//分配内存,返回一个string和切片,二者共享内存空间s, b := rawstringtmp(buf, l)//string无法修改,只能通过[]byte来修改for _, x := range a {copy(b, x)b = b[len(x):]}return s
}

因为string无法直接修改,所以这里使用rawstringtmp()函数初始化一个指定的大小的string,同时返回一个切片,二者共享同一块内存空间,后者向切片中拷贝数据,也就间接的修改了string。

rawstringtmp()函数

func rawstringtmp(buf *tmpBuf, l int) (s string, b []byte) {if buf != nil && l <= len(buf) {b = buf[:l]s = slicebytetostringtmp(&b[0], len(b))} else {s, b = rawstring(l)}return
}

rawstring()函数

//生成一个新的string,返回的string和切片共享相同的空间
// rawstring allocates storage for a new string. The returned
// string and byte slice both refer to the same storage.
// The storage is not zeroed. Callers should use
// b to set the string contents and then drop b.
func rawstring(size int) (s string, b []byte) {p := mallocgc(uintptr(size), nil, false)stringStructOf(&s).str = pstringStructOf(&s).len = size*(*slice)(unsafe.Pointer(&b)) = slice{p, size, size}return
}

类型转换

[]byte转string

byte切片可以很方便地转成string:

func GetStringBySlice(s []byte)string {return string(s)
}

需要注意的是:这种转换需要一次内存拷贝。

转换过程如下:

  • 根据切片的长度申请内存空间,假设内存地址为p,长度为len
  • 构建string(string.str = p , string.len = len )
  • 拷贝数据(切片中将数据拷贝到新申请的内存空间)

转换示意图如下所示:

在runtime包中使用slicebytetostring()函数将[]byte转成string。

slicebytetostring() 函数如下:

// slicebytetostring converts a byte slice to a string.
// It is inserted by the compiler into generated code.
// ptr is a pointer to the first element of the slice;
// n is the length of the slice.
// Buf is a fixed-size buffer for the result,
// it is not nil if the result does not escape.
func slicebytetostring(buf *tmpBuf, ptr *byte, n int) (str string) {if n == 0 {// Turns out to be a relatively common case.// Consider that you want to parse out data between parens in "foo()bar",// you find the indices and convert the subslice to string.return ""}if raceenabled {racereadrangepc(unsafe.Pointer(ptr),uintptr(n),getcallerpc(),funcPC(slicebytetostring))}if msanenabled {msanread(unsafe.Pointer(ptr), uintptr(n))}if n == 1 {p := unsafe.Pointer(&staticuint64s[*ptr])if sys.BigEndian {p = add(p, 7)}stringStructOf(&str).str = pstringStructOf(&str).len = 1return}var p unsafe.Pointerif buf != nil && n <= len(buf) {//如果预留buf够用,则用预留的bufp = unsafe.Pointer(buf)} else {//否则重新申请内存p = mallocgc(uintptr(n), nil, false)}//构建字符串stringStructOf(&str).str = pstringStructOf(&str).len = n//将切片底层数组中数据拷贝到字符串中memmove(p, unsafe.Pointer(ptr), uintptr(n))return
}

slicebytetostring()函数会优先使用一个固定大小的buf,当buf长度不够时才会申请新的内存,这样子避免了内存空间浪费。

string转[]byte

string也可以很方便的转成byte切片

func GetSliceByString(str string) []byte {return []byte(str)
}

string转成byte切片同样也需要一次内存拷贝的动作,其过程如下:

  • 申请切片内存空间
  • 将string拷贝到切片中

转换示意图如下所示:

在runtime包中,使用stringtoslicebyte()函数将string转成[]byte

stringtoslicebyte()函数如下:

func stringtoslicebyte(buf *tmpBuf, s string) []byte {var b []byteif buf != nil && len(s) <= len(buf) {*buf = tmpBuf{}//从预留buf中切出新的切片b = buf[:len(s)]} else {//生成新的切片b = rawbyteslice(len(s))}copy(b, s)return b
}
// rawbyteslice allocates a new byte slice. The byte slice is not zeroed.
func rawbyteslice(size int) (b []byte) {cap := roundupsize(uintptr(size))p := mallocgc(cap, nil, false)if cap != uintptr(size) {memclrNoHeapPointers(add(p, uintptr(size)), cap-uintptr(size))}*(*slice)(unsafe.Pointer(&b)) = slice{p, size, int(cap)}return
}

stringtoslicebyte()函数中也使用了预留buf,并只在该buf长度不够时才会申请内存,其中rawbyteslice()函数用于申请新的未初始化的切片。由于字符串内容将完整覆盖切片的存储空间,所以可以不初始化切片从而提升分配效率。

编译优化

byte切片转换成string的场景很多,出于性能上的考虑,有时候只是应用于在临时需要字符串的场景下,byte切片转换成string时并不会拷贝内存,而是直接返回一个string,这个string的指针(string.str)指向切片的内存地址。

比如,编译器会识别如下临时场景:

  • 使用m[string(b)]来查找map(map中的key类型是string时,临时把切片b转成string)
  • 字符串拼接,如<" + “string(b)” + ">
  • 字符串比较: string(b) == “foo”

由于只是临时把byte切片转换成string,也就避免了因byte切片内容修改而导致string数据变化的问题,所以此时可以不必拷贝内存。

小结

为何不允许修改字符串

像C++语言中的string,其本身拥有内存空间,修改string是支持的。但在Go语言的实现中,string不包含内存空间,只有一个内存的指针和长度,这样做的好处是string变得非常轻量级,可以很方便地进行传递而不用担心内存拷贝。

因为string通常指向字符串字面量,而字符串字面量存储的位置是只读段,而不是堆或者栈上,所以才有了string不可修改的约定。

string和[]byte如何取舍

string和[]byte都可以表示字符串,但是因为数据结构不同,其衍生出来的方法也不一样,要根据具体的场景选择不同的结构来使用。

string擅长的场景:

  • 需要字符串比较的场景
  • 不需要nil字符串的场景

[]byte擅长的场景:

  • 修改字符串,尤其是修改粒度为1个字节的场景
  • 函数返回值,需要用nil表示含义的场景
  • 需要切片操作的场景

虽然看起来string使用的场景不多,但是因为string直观,在实际应用中还是大量存在的,相对而言,在偏底层的实现中[]byte使用的更多一些。

golang数据结构初探之字符串string相关推荐

  1. golang数据结构初探之管道chan

    golang数据结构初探之管道chan 管道是go在语言层面提供的协程之间的通信方式,比unix的管道更易用也更轻便. 特效速览 初始化 声明和初始化管道的方式主要有以下两种: 变量声明 使用内置函数 ...

  2. golang数据结构初探之动态数组slice

    动态数组slice slice 又称动态数组,依托于数组实现,可以方便的进行扩容和传递,实际使用时比数组更灵活.但正是因为灵活,实际使用时更容易出错,避免出错的最好方法便是了解其实现原理. 特性速览 ...

  3. golang数据结构初探之字典map

    Map Go语言的map底层使用Hash表实现的 特性预览 操作方式 初始化 map分别支持字面量初始化和内置函数make()初始化 字面量初始化 func MapInit() {m := map[s ...

  4. golang数据结构初探之iota

    iota Go语言的iota常用于const表达式中,其值是从0开始的,const声明块中每增加一行,iota值都会自增1. 使用iota可以简化常量的定义,但其规则必须牢记,否则在阅读源码时可能会造 ...

  5. Redis实战(2)-数据结构之字符串String实战之存储对象

    概述:本系列博文所涉及的相关内容来源于debug亲自录制的实战课程:缓存中间件Redis技术入门与应用场景实战(SpringBoot2.x + 抢红包系统设计与实战),感兴趣的小伙伴可以点击自行前往学 ...

  6. Go 学习笔记(31)— 字符串 string、字符 rune、字节 byte、UTF-8 和 Unicode 区别以及获取字符串长度

    1. 字符串 string 类型 Go 语言中字符串的内部实现使用 UTF-8 编码,通过 rune 类型,可以方便地对每个 UTF-8 字符进行访问.当然, Go 语言也支持按照传统的 ASCII ...

  7. Golang源码阅读笔记 - String

    String用法说明 在src/buildin/buildin.go文件中,对golang内建数据类型做了详细的描述,关于string的说明如下: // string is the set of al ...

  8. Golang中获取中文字符串的子串字符位置及截取子串

    Golang中获取中文字符串的子串字符位置及截取子串 版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明. 本文链接:https://blog.cs ...

  9. Golang几种连接字符串方法

    Golang几种连接字符串方法 Golang中字符串是不可变的使用UTF-8编码任意字节链.把一个或多个字符相加称为字符串连接.最简单的方式使用+操作符,本文介绍多种方式连接字符串. 1. 使用加操作 ...

最新文章

  1. BeautifulSoup操作xml文件
  2. 谷歌地图将很快显示电动汽车充电站
  3. 基于mini2440的ov9650摄像头裸机测试
  4. java中线程死锁及避免_如何避免Java线程中的死锁?
  5. 环境变量的配置windows10系统
  6. const int和const int本质区别
  7. ROS学习笔记五:理解ROS topics
  8. mysql 范围索引 els_MySQL 复习笔记
  9. 九、Linux 软件包安装
  10. Kotlin基础学习-入门篇
  11. springboot项目版本升级
  12. arch linux u盘安装,如何把ArchLinux安装到U盘上
  13. 计量经济学及Stata应用 陈强 第七章异方差习题7.3
  14. 批量检测百度云分享链接有效性方法
  15. 一些获取免费域名的方法
  16. 3d album android下载,声影制作家3D-Album
  17. 数字信号处理翻转课堂笔记10
  18. Pundit的Rails授权
  19. 【Python学习笔记】结巴分词
  20. #TP4056#--3.7V锂电池充放电电路(实践日志篇)

热门文章

  1. 关于eNSP路由器无法启动问题的解决
  2. 显示器的长宽比主要有哪几种比例,以及他们对应的分辨率?
  3. 字符串压缩算法5.11
  4. 在线图片翻转、旋转工具
  5. UWB定位技术的由来
  6. 【转载】jsDelivr的一些替代方案
  7. memset函数的实现方式
  8. 【idea】tag(git标签)的使用
  9. 用DIV+CSS技术设计的个人电影网站-web前端网页制作课作业---电影介绍 5页
  10. ubuntu密码更改、关闭sudo密码