头图

Go String 解析

白菜1031北京市
English

一、String 结构定义

// src/runtime/string.go:stringStruct

type stringStruct struct {
    str unsafe.Pointer
    len int
}

String结构定义

String 类型在Go语言内存模型中其实是一个“描述符”,用一个2字节的数据结构表示,它本身并不真正存储字符串数据,而仅是由一个指向底层存储的指针和字符串的长度字段组成的。

str:指向字符串底层存储首地址的指针,1字节。
len:字符串的长度,1字节。

因此,我们即便直接将 String 类型变量作为函数参数,其传递的开销也是恒定的,不会随着字符串大小的变化而变化。

二、String 特性

1. String 类型的值在它的生命周期内不可改变

type stringStruct struct {
    str unsafe.Pointer
    len int
}

func main() {
    var s string = "hello"

    s[0] = 'a' //错误:无法给s[0]赋值,因为字符串内容是不可改变的

    fmt.Printf("%#v\n", (*stringStruct)(unsafe.Pointer(&s))) 
    //输出:&main.stringStruct{str:(unsafe.Pointer)(0x2b599c), len:5}

    s = "world" //修改字符串,字符串底层结构体中str指针已经发生变化
    
    fmt.Printf("%#v\n", (*stringStruct)(unsafe.Pointer(&s))) 
    //输出:&main.stringStruct{str:(unsafe.Pointer)(0x2b5a00), len:5}
    
    fmt.Printf("%#v\n", (*[5]byte)((*stringStruct)(unsafe.Pointer(&s)).str)) 
    //输出:&[5]uint8{0x77, 0x6f, 0x72, 0x6c, 0x64} 分别对应 w o r l d 的 ASCII 码
}
由于runtime.stringStruct结构是非导出的,不能直接使用,所以手动定义了一个stringStruct结构体。

String 类型的数据不可改变的特性,提高了字符串的并发安全性存储利用率

  • 字符串可以被多个协程共享,开发者不用再担心字符串的并发安全问题。
  • 针对同一个字符串值,无论它在程序的几个位置被使用,编译器只需要为它分配一块存储,大大提高了存储利用率。

2. 没有结尾’\0’,存储了字符串长度

Go 字符串中没有结尾’\0’,并且存储了字符串长度,获取字符串长度的时间复杂度是常数,无论字符串中字符个数有多少,我们都可以快速得到字符串的长度值。

3. String 可以是空的"",但不能是 nil

var s string = ""
s = nil // 错误

4. 对非 ASCII 字符提供原生支持,消除了源码在不同环境下显示乱码的可能

Go 语言源文件默认采用的是 Unicode 字符集,Unicode 字符集是目前市面上最流行的字符集,它囊括了几乎所有主流非 ASCII 字符(包括中文字符)。
Go 字符串中的每个字符都是一个 Unicode 字符,并且这些 Unicode 字符是以 UTF-8 编码格式存储在内存当中的。

5. 原生支持“所见即所得”的原始字符串,大大降低构造多行字符串时的心智负担。

var s string = `         ,_---~~~~~----._
    _,,_,*^____      _____*g*\"*,--,
   / __/ /'     ^.  /      \ ^@q   f
  [  @f | @))    |  | @))   l  0 _/
   \/   \~____ / __ \_____/     \
    |           _l__l_           I
    }          [______]           I
    ]            | | |            |
    ]             ~ ~             |
    |                            |
     |                           |`
fmt.Println(s)

三、String 常规操作

1. 下标操作

在字符串的实现中,真正存储数据的是底层的数组。字符串的下标操作本质上等价于底层数组的下标操作。我们在前面的代码中实际碰到过针对字符串的下标操作,形式是这样的:

var s = "乘风破浪"
fmt.Printf("0x%x\n", s[0]) // 0xe4:字符“乘” utf-8编码的第一个字节

我们可以看到,通过下标操作,我们获取的是字符串中特定下标上的字节,而不是字符。

2. 字符迭代

Go 有两种迭代形式:常规 for 迭代for range 迭代
通过这两种形式的迭代对字符串进行操作得到的结果是不同的。

通过常规 for 迭代对字符串进行的操作是一种字节视角的迭代,每轮迭代得到的的结果都是组成字符串内容的一个字节,以及该字节所在的下标值,这也等价于对字符串底层数组的迭代:

var s = "乘风破浪"
for i := 0; i < len(s); i++ {
    fmt.Printf("index: %d, value: 0x%x\n", i, s[i])
}

输出:

index: 0, value: 0xe4
index: 1, value: 0xb9
index: 2, value: 0x98 // "\xe4\xb9\x98" 乘
index: 3, value: 0xe9
index: 4, value: 0xa3
index: 5, value: 0x8e // "\xe9\xa3\x8e" 风
index: 6, value: 0xe7
index: 7, value: 0xa0
index: 8, value: 0xb4 // "\xe7\xa0\xb4" 破
index: 9, value: 0xe6
index: 10, value: 0xb5
index: 11, value: 0xaa // "\xe6\xb5\xaa" 浪

通过 for range 迭代,我们每轮迭代得到的是字符串中 Unicode 字符的码点值,以及该字符在字符串中的偏移值

var s = "乘风破浪"
for i, v := range s {
    fmt.Printf("index: %d, value: 0x%x\n", i, v)
}

输出:

index: 0, value: 0x4e58
index: 3, value: 0x98ce
index: 6, value: 0x7834
index: 9, value: 0x6d6a

3. 字符串连接

虽然通过 +/+= 进行字符串连接的开发体验是最好的,但连接性能就未必是最快的了。
Go 还提供了 strings.Builder、strings.Join、fmt.Sprintf 等函数来进行字符串连接操作。

4. 字符串比较

Go 字符串类型支持各种比较关系操作符,包括 ==!=>=<=><。在字符串的比较上,Go 采用字典序的比较策略,分别从每个字符串的起始处,开始逐个字节地对两个字符串类型变量进行比较。
当两个字符串之间出现了第一个不相同的元素,比较就结束了,这两个元素的比较结果就会做为串最终的比较结果。如果出现两个字符串长度不同的情况,长度比较小的字符串会用空元素补齐,空元素比其他非空元素都小。

如果两个字符串的长度不相同,那么我们不需要比较具体字符串数据,也可以断定两个字符串是不同的。但是如果两个字符串长度相同,就要进一步判断,数据指针是否指向同一块底层存储数据。如果还相同,那么我们可以说两个字符串是等价的,如果不同,那就还需要进一步去比对实际的数据内容。

func main() {
    // ==
    s1 := "乘风破浪"
    s2 := "乘风" + "破浪"
    fmt.Println(s1 == s2) // true

    // !=
    s1 = "Go"
    s2 = "PHP"
    fmt.Println(s1 != s2) // true

    // < and <=
    s1 = "12345"
    s2 = "23456"
    fmt.Println(s1 < s2)  // true
    fmt.Println(s1 <= s2) // true

    // > and >=
    s1 = "12345"
    s2 = "123"
    fmt.Println(s1 > s2)  // true
    fmt.Println(s1 >= s2) // true
}

第五个操作:字符串转换。

Go 支持字符串与字节切片、字符串与 rune 切片的双向转换,并且这种转换无需调用任何函数,只需使用显式类型转换就可以了

var s = "乘风破浪"

// string -> []rune
rs := []rune(s)
fmt.Printf("%x\n", rs) // [4e58 98ce 7834 6d6a]

// string -> []byte
bs := []byte(s)
fmt.Printf("%x\n", bs) // e4b998e9a38ee7a0b4e6b5aa

// []rune -> string
s1 := string(rs)
fmt.Println(s1) // 乘风破浪

// []byte -> string
s2 := string(bs)
fmt.Println(s2) // 乘风破浪
阅读 520

全栈札记
欢迎关注同名公众号。全栈工程师成长札记。 主要记录MySQL、Redis、ElasticSearch、MongoDB、Linux、She...
5.4k 声望
1.6k 粉丝
0 条评论
5.4k 声望
1.6k 粉丝
文章目录
宣传栏