mirror of
https://github.com/chai2010/advanced-go-programming-book.git
synced 2025-05-23 20:02:22 +00:00
commit
e46fca62ff
@ -1,8 +1,8 @@
|
||||
# 1.3. 数组、字符串和切片
|
||||
|
||||
在主流的编程语言中数组和数组相关的数据结构是使用最频繁的数据类型,只有在数组结构不能满足时才会考虑链表、hash表(hash表可以看作是数组和链表的混合体)和更复杂的自定义数据结构。
|
||||
在主流的编程语言中数组及其相关的数据结构是使用得最为频繁的,只有在它(们)不能满足时才会考虑链表、hash表(hash表可以看作是数组和链表的混合体)和更复杂的自定义数据结构。
|
||||
|
||||
在Go语言中数组、字符串和切片三者是密切相关的数据结构。三个数据类型的底层原始数据有着相同的内存结构,但是因为上层语法的限制而导致有着不同的行为。首先,Go语言的数组是一种值类型,虽然数组的元素可以被修改,但是数组本身的赋值和函数传参数都是整体复制的方式处理的。Go语言字符串底层数据也是对应字节数组,但是字符串只读属性禁止在程序中修改底层字节数组的元素,字符串赋值并不会导致复制底层的数据,只是复制字符串底层数据地址和对应的长度。而切片的行为更为灵活,切片的结构和字符串结构类似,但是解除了字符串只读的限制。切片的底层数据虽然也是对应数据类型的数组,但是每个切片还有独立的长度和容量信息,切片赋值和函数传参数时也是以切片头信息部分传值方式处理。因为切片头含有底层数据在指针,切片的赋值也不会导致底层数据的复制操作。其实Go语言的赋值和函数传参规则很简单,除了通过闭包函数对外部变量是以引用的方式访问之外,其它赋值和函数传参数都是以传值的方式处理。要理解数组、字符串和切片三种不同的处理方式的原因需要详细了解它们的底层数据结构。
|
||||
Go语言中数组、字符串和切片三者是密切相关的数据结构。这三种数据类型,在底层原始数据有着相同的内存结构,在上层,因为语法的限制而有着不同的行为表现。首先,Go语言的数组是一种值类型,虽然数组的元素可以被修改,但是数组本身的赋值和函数传参都是以整体复制的方式处理的。Go语言字符串底层数据也是对应的字节数组,但是字符串的只读属性禁止了在程序中对底层字节数组的元素的修改。字符串赋值只是复制了数据地址和对应的长度,而不会导致底层数据的复制。切片的行为更为灵活,切片的结构和字符串结构类似,但是解除了只读限制。切片的底层数据虽然也是对应数据类型的数组,但是每个切片还有独立的长度和容量信息,切片赋值和函数传参数时也是将切片头信息部分按传值方式处理。因为切片头含有底层数据的指针,所以它的赋值也不会导致底层数据的复制。其实Go语言的赋值和函数传参规则很简单,除了闭包函数以引用的方式对外部变量访问之外,其它赋值和函数传参数都是以传值的方式处理。要理解数组、字符串和切片三种不同的处理方式的原因需要详细了解它们的底层数据结构。
|
||||
|
||||
## 数组
|
||||
|
||||
@ -121,7 +121,7 @@ var f = [...]int{} // 定义一个长度为0的数组
|
||||
<-c1
|
||||
```
|
||||
|
||||
在这里,我们并不关心管道中传输数据的真实类型,其中管道接收和发送操作只是用于消息的同步。对于这种场景,我们用空数组来作为管道类型可以减少管道元素赋值时的开销。当然一般更倾向与用无类型的匿名结构体代替:
|
||||
在这里,我们并不关心管道中传输数据的真实类型,其中管道接收和发送操作只是用于消息的同步。对于这种场景,我们用空数组来作为管道类型可以减少管道元素赋值时的开销。当然一般更倾向于用无类型的匿名结构体代替:
|
||||
|
||||
```go
|
||||
c2 := make(chan struct{})
|
||||
@ -154,7 +154,7 @@ type StringHeader struct {
|
||||
}
|
||||
```
|
||||
|
||||
字符串结构有两个信息组成:第一个是字符串指向的底层字节数据,第二个是字符串的字节长度。字符串其实是一个结构体,因此字符串的赋值操作也就是`reflect.StringHeader`结构体的复制过程,并不会涉及底层字节数组的复制。在前面数组一节提到的`[2]string`字符串数组对应的底层结构和`[2]reflect.StringHeader`对应的底层结构是一样的,可以将字符串数组看作一个结构体数组。
|
||||
字符串结构由两个信息组成:第一个是字符串指向的底层字节数组,第二个是字符串的字节的长度。字符串其实是一个结构体,因此字符串的赋值操作也就是`reflect.StringHeader`结构体的复制过程,并不会涉及底层字节数组的复制。在前面数组一节提到的`[2]string`字符串数组对应的底层结构和`[2]reflect.StringHeader`对应的底层结构是一样的,可以将字符串数组看作一个结构体数组。
|
||||
|
||||
我们可以看看字符串“Hello, world”本身对应的内存结构:
|
||||
|
||||
@ -257,7 +257,7 @@ fmt.Printf("%#v\n", []rune("Hello, 世界")) // []int32{19990, 30028}
|
||||
fmt.Printf("%#v\n", string([]rune{'世', '界'})) // 世界
|
||||
```
|
||||
|
||||
从上面代码的输出结果看,我们可以发现`[]rune`其实是`[]int32`类型,这里的`rune`只是`int32`类型的别名,并不是重新定义的类型。`rune`用于表示每个UNICODE码点,目前只使用了21个bit位。
|
||||
从上面代码的输出结果来看,我们可以发现`[]rune`其实是`[]int32`类型,这里的`rune`只是`int32`类型的别名,并不是重新定义的类型。`rune`用于表示每个UNICODE码点,目前只使用了21个bit位。
|
||||
|
||||
字符串相关的强制类型转换主要涉及到`[]byte`和`[]rune`两种类型。每个转换都可能隐含重新分配内存的代价,最坏的情况下它们的运算时间复杂度都是`O(n)`。不过字符串和`[]rune`的转换要更为特殊一些,因为一般这种强制类型转换要求两个类型的底层内存结构要尽量一致,显然它们底层对应的`[]byte`和`[]int32`类型是完全不同的内部布局,因此这种转换可能隐含重新分配内存的操作。
|
||||
|
||||
@ -351,7 +351,7 @@ func runes2string(s []int32) string {
|
||||
|
||||
```go
|
||||
type SliceHeader struct {
|
||||
Data uintptr
|
||||
Data uintptr
|
||||
Len int
|
||||
Cap int
|
||||
}
|
||||
@ -607,5 +607,5 @@ func SortFloat64FastV2(a []float64) {
|
||||
|
||||
第二种转换操作是分别取到两个不同类型的切片头信息指针,任何类型的切片头部信息底层都是对应`reflect.SliceHeader`结构,然后通过更新结构体方式来更新切片信息,从而实现`a`对应的`[]float64`切片到`c`对应的`[]int`类型切片的转换。
|
||||
|
||||
通过基准测试,我们可以发现用`sort.Ints`对转换后的`[]int`排序的性能要比用`sort.Float64s`排序的性能好一点。不过需要注意的是,这个方法可行的前提是要保证`[]float64`中没有Nan和Inf等非规范的浮点数(因为浮点数中Nan不可排序,正0和负0相等,但是整数中没有这类情形)。
|
||||
通过基准测试,我们可以发现用`sort.Ints`对转换后的`[]int`排序的性能要比用`sort.Float64s`排序的性能好一点。不过需要注意的是,这个方法可行的前提是要保证`[]float64`中没有NaN和Inf等非规范的浮点数(因为浮点数中NaN不可排序,正0和负0相等,但是整数中没有这类情形)。
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user