文章目录
回顾基础语法(1)
完整链接:【GO基础学习】环境安装到基础语法(1)
回顾&总结:
- 一个Go工程中主要包含以下三个目录:
src:源代码文件
pkg:包文件(用于生成.a文件)
bin:相关bin文件(用于生成可执行文件)
golang中的import name,实际是到GOPATH中去寻找name.a, 使用时是该name.a的源码中生命的package 名字
- 编译主程序
go build goproject.go
=>成功后会生成goproject.exe
文件 - 系统编译时
go install abc_name
时,系统会到GOPATH的src
目录中寻找abc_name
目录,然后编译其下的go文件。
报错:
java
C:\Users\Administrator>go install GoStudy
go: 'go install' requires a version when current directory is not in a module
Try 'go install GoStudy@latest' to install the latest version
go build
和go install
的区别
- go build:编译 Go 项目中的代码并生成可执行文件,但并不会将该文件安装到 GOBIN 目录中。如果只是为了编译代码并在当前目录运行该程序,使用 go build。
- go build 会根据当前目录下的文件生成一个可执行文件(Windows 上会生成 .exe 文件)。它不会改变你的工作空间,编译过程只在当前目录中完成,不会将编译后的程序安装到系统的 GOBIN 目录下。
- go install:编译并安装 Go 项目中的代码。安装后,程序会被放入 GOBIN 目录(如果设置了该变量),并且你可以从任何地方运行它(如果 GOBIN 在环境变量 PATH 中)。go install 相当于先执行 go build,然后将可执行文件放入指定的 GOBIN 路径。
- go build 会根据当前目录下的文件生成一个可执行文件(Windows 上会生成 .exe 文件)。它不会改变你的工作空间,编译过程只在当前目录中完成,不会将编译后的程序安装到系统的 GOBIN 目录下。
- go install:编译并安装 Go 项目中的代码。安装后,程序会被放入 GOBIN 目录(如果设置了该变量),并且你可以从任何地方运行它(如果 GOBIN 在环境变量 PATH 中)。go install 相当于先执行 go build,然后将可执行文件放入指定的 GOBIN 路径。
【解决:进入项目目录,然后执行go build
后会在当前目录生成.exe
文件】
在 Go 1.11 及之后版本,Go 引入了 Go Modules
(通过 go.mod
【go mod init GoStudy
】文件管理),而在 Go Modules 模式下,go build 默认会在当前目录生成可执行文件,而不是 GOPATH/bin 目录。这个行为与传统的 GOPATH 工作方式有所不同。
- 变量申明:
var 变量名 变量类型
例如:var name string;var age int;var isOk bool
,不指定的情况下,整型和浮点型变量的默认值为0。 字符串变量的默认值为空字符串。 布尔型变量默认为false。 切片、函数、指针变量的默认为nil。自行初始化格式:var 变量名 类型 = 表达式
在函数内部,可以使用更简略的 :=
方式声明并初始化变量:
go
package main
import (
"fmt"
)
// 全局变量m
var m = 100
func main() {
n := 10
m := 200 // 此处声明局部变量m
fmt.Println(m, n)
}
-
常量定义:
const pi = 3.1415
,常量在定义的时候必须赋值。 -
字符串操作:
方法 | 介绍 |
---|---|
len(str) | 求长度 |
+或fmt.Sprintf | 拼接字符串 |
strings.Split | 分割 |
strings.Contains | 判断是否包含 |
strings.HasPrefix,strings.HasSuffix | 前缀/后缀判断 |
strings.Index(),strings.LastIndex() | 子串出现的位置 |
strings.Join(a[]string, sep string) | join操作 |
uint8
类型,或者叫 byte
型,代表了ASCII码的一个字符。
rune
类型,代表一个 UTF-8字符。
- 数组的操作:
(1)数组定义:var a [len]int
,比如:var a [5]int
,数组长度必须是常量,且是类型的组成部分。一旦定义,长度不能变。
(2)数组可以通过下标进行访问,下标是从0开始,最后一个元素下标是:len-1
go
for i := 0; i < len(a); i++ {
}
for index, v := range a {
}
(3)数组是值类型 ,赋值和传参会复制整个数组,而不是指针。因此改变副本的值,不会改变本身的值。【与JAVA不同的点,JAVA的数组是引用类型】
(4)具体使用:
go
全局:
var arr0 [5]int = [5]int{1, 2, 3}
var arr1 = [5]int{1, 2, 3, 4, 5}
var arr2 = [...]int{1, 2, 3, 4, 5, 6}
var str = [5]string{3: "hello world", 4: "tom"}
局部:
a := [3]int{1, 2} // 未初始化元素值为 0。
b := [...]int{1, 2, 3, 4} // 通过初始化值确定数组长度。
c := [5]int{2: 100, 4: 200} // 使用索引号初始化元素。
d := [...]struct {
name string
age uint8
}{
{"user1", 10}, // 可省略元素类型。
{"user2", 20}, // 别忘了最后一行的逗号。
}
全局:
var arr0 [5][3]int
var arr1 [2][3]int = [...][3]int{{1, 2, 3}, {7, 8, 9}}
局部:
a := [2][3]int{{1, 2, 3}, {4, 5, 6}}
b := [...][2]int{{1, 1}, {2, 2}, {3, 3}} // 第 2 纬度不能用 "..."。
go
package main
import "fmt"
func main() {
var arr0 [5][3]int
arr1 := [5][3]int{}
arr2 := [3][3]int{{1, 2, 3}, {2, 3, 4}, {4, 5, 6}}
fmt.Println(arr0, arr1, arr2)
}
输出
bash
[[0 0 0] [0 0 0] [0 0 0] [0 0 0] [0 0 0]] [[0 0 0] [0 0 0] [0 0 0] [0 0 0] [0 0 0]] [[1 2 3] [2 3 4] [4 5 6]]
(5)内置函数 len
和 cap
都返回数组长度 (元素数量)
java
package main
import "fmt"
func main() {
arr1 := [5][3]int{{2, 3, 4}}
fmt.Println(arr1)
arr2 := [...]int{1, 2, 3}
fmt.Println(arr2)
fmt.Println(len(arr1), cap(arr1))
fmt.Println(len(arr2), cap(arr2))
}
bash
[[2 3 4] [0 0 0] [0 0 0] [0 0 0] [0 0 0]]
[1 2 3]
5 5
3 3
(7)指针数组
java
package main
import "fmt"
func printArr(arr *[5]int) {
arr[0] = 10
for i, v := range arr {
fmt.Println(i, v)
}
}
func main() {
var arr1 [5]int
printArr(&arr1)
fmt.Println(arr1)
arr2 := [...]int{2, 4, 6, 8, 10}
printArr(&arr2)
fmt.Println(arr2)
}
传入的是&arr1
引用,所以printArr
方法不是拷贝,会改变原数组的值。
- 切片Slice的使用:切片是数组的一个引用,因此切片是引用类型。但自身是结构体,值拷贝传递。
(1)动态长度 :切片是基于数组的,但它的长度可以动态变化。切片底层的数据实际存储在一个数组中,但切片自己并不拥有数据。
(2)引用类型 :切片是一个引用类型,指向一个底层数组的某段区域 。当切片被赋值或传递时,引用的是相同的底层数组 。
(3)内存结构 :切片在 Go 中是一个结构体,包含指向底层数组的指针(ptr) 、切片的长度(len)和容量(cap) 。
(4)切片的定义主要两种方式:可以不指定长度,通过 make
创建,也可以直接通过字面量方式创建。== nil
判断是否为空
go
slice := []int{1, 2, 3} // 长度为3,容量为3
slice := make([]int, 3, 5) // 创建长度为3,容量为5的切片
切片的长度和容量可以动态变化,长度表示切片实际存储的元素数量,容量表示切片在底层数组中可以容纳的最大元素数。
go
package main
import "fmt"
func printArr(arr []int) {
arr[0] = 10
for i, v := range arr {
fmt.Println(i, v)
}
}
func main() {
arr1 := []int{1, 2, 3}
printArr(arr1)
fmt.Println(arr1)
}
arr1
是切片的定义,传入方法的是引用,会改变原切片的值。
(5)切片其实是一个结构体,包含三个字段:
- 指针(Pointer):指向底层数组某个位置的指针。
- 长度(Length):切片实际存储的元素个数。
- 容量(Capacity):从切片的起始位置到底层数组结尾的元素个数。
(6)切片的扩容
当切片需要添加元素而容量不足时,Go 会创建一个新的、容量更大的底层数组,并将原数组的内容复制过去。这种情况下,切片指向的底层数组就变成了新数组,避免了频繁的数组拷贝操作。
(7)相关操作:
(8)make创建slice:
go
package main
import "fmt"
func main() {
slice := make([]int, 3, 5)
slice[0] = 1
fmt.Println(slice)
slice[3] = 3
fmt.Println(slice)
}
报错:
go
[1 0 0]
panic: runtime error: index out of range [3] with length 3
goroutine 1 [running]:
main.main()
E:/goproject/src/GoStudy/main.go:9 +0x85
Process finished with the exit code 2
当前切片的长度为 3,索引 3 超出了切片的当前长度(len(slice)),因此会引发运行时错误 "index out of range"。想要添加元素到切片中,可以使用 append 函数,而不是直接赋值。
go
package main
import "fmt"
func main() {
slice := make([]int, 3, 5)
slice[0] = 1
fmt.Println(slice)
// 使用 append 添加元素,而不是直接赋值
slice = append(slice, 3)
fmt.Println(slice)
}
但是,append 方法默认只能在切片的末尾添加元素,它会将新元素追加到切片的当前长度(len(slice))的位置。这是 append 的特性,也是为什么切片通常是动态、顺序增长的数据结构。
关于append
方法更多操作:
go
// 追加元素到切片末尾
slice = append(slice, 4) // 追加单个元素
slice = append(slice, 5, 6, 7) // 追加多个元素
// 追加一个切片到另一个切片
slice1 = append(slice1, slice2...) // 将 slice2 追加到 slice1
(9)在任意位置插入元素
go
package main
import "fmt"
func main() {
slice := []int{1, 2, 3, 4}
insertIndex := 2
element := 99
// 在索引 2 位置插入 99
slice = append(slice[:insertIndex], append([]int{element}, slice[insertIndex:]...)...)
fmt.Println(slice) // 输出:[1 2 99 3 4]
}
(10)删除指定位置的元素
go
package main
import "fmt"
func main() {
slice := []int{1, 2, 3, 4, 5}
deleteIndex := 2
slice = append(slice[:deleteIndex], slice[deleteIndex+1:]...)
fmt.Println(slice)
}
(11)删除切片的首尾元素
go
package main
import "fmt"
func main() {
slice := []int{1, 2, 3, 4, 5}
slice = slice[1 : len(slice)-1]
fmt.Println(slice)
}
(12)动态扩容
go
package main
import "fmt"
func main() {
slice := make([]int, 3, 5) // 长度为 3,容量为 5
slice[0], slice[1], slice[2] = 1, 2, 3
fmt.Println(slice, cap(slice)) // 输出:[1 2 3] 5
// 添加元素,超出原容量时扩展
slice = append(slice, 4, 5, 6)
fmt.Println(slice, cap(slice)) // 输出:[1 2 3 4 5 6] 10(容量翻倍)
}
go
package main
import "fmt"
func main() {
slice := []int{1, 2, 3, 4, 5}
slice = append(slice, 6)
fmt.Println(slice, cap(slice)) // 输出: [1 2 3 4 5 6] 10
}
(13)其他常见操作
- 切片可以通过切片操作符
[:]
来截取部分内容:左闭右开 - 切片为空的判断条件是
len(slice) == 0
,而不是slice == nil
,因为长度为 0 的切片并不一定是 nil。
go
package main
import "fmt"
func main() {
var slice1 []int // 未初始化,为 nil
slice2 := make([]int, 0) // 初始化,但长度为 0
fmt.Println(slice1 == nil) // 输出:true
fmt.Println(len(slice2)) // 输出:0
fmt.Println(slice2 == nil) // 输出:false
}
Slice底层实现
切片是 Go 中的一种基本的数据结构,使用这种结构可以用来管理数据集合。切片的设计想法是由动态数组概念而来,为了开发者可以更加方便的使一个数据结构可以自动增加和减少。但是切片本身并不是动态数据或者数组指针。切片常见的操作有 reslice
、append
、copy
。与此同时,切片还具有可索引,可迭代的优秀特性。
切片和数组:
在 Go 中,与 C 数组变量隐式作为指针使用不同,Go 数组是值类型,赋值和函数传参操作都会复制整个数组数据。
go
package main
import (
"fmt"
)
func main() {
arrayA := [2]int{100, 200}
var arrayB [2]int
arrayB = arrayA
fmt.Printf("arrayA : %p , %v\n", &arrayA, arrayA) // arrayA : 0xc00000a0f0 , [100 200]
fmt.Printf("arrayB : %p , %v\n", &arrayB, arrayB) // arrayB : 0xc00000a100 , [100 200]
testArray(arrayA)
}
func testArray(x [2]int) {
fmt.Printf("func Array : %p , %v\n", &x, x) // func Array : 0xc00000a150 , [100 200]
}
可以看到,三个内存地址都不同,这也就验证了 GO 中数组赋值和函数传参都是值复制的。那这会导致什么问题呢?
假想每次传参都用数组,那么每次数组都要被复制一遍。如果数组大小有 100万,在64位机器上就需要花费大约 800W 字节,即 8MB 内存。这样会消耗掉大量的内存。于是乎有人想到,函数传参用数组的指针。
go
package main
import (
"fmt"
)
func main() {
arrayA := []int{100, 200}
testArrayPoint(&arrayA) // 1.传数组指针 func Array : 0xc0000081b0 , [100 200]
arrayB := arrayA[:]
testArrayPoint(&arrayB) // 2.传切片 func Array : 0xc0000081e0 , [100 300]
fmt.Printf("arrayA : %p , %v\n", &arrayA, arrayA) // arrayA : 0xc0000081b0 , [100 400]
}
func testArrayPoint(x *[]int) {
fmt.Printf("func Array : %p , %v\n", x, *x)
(*x)[1] += 100
}
总结
- 切片和指针:通过指针传递切片允许在函数中修改原始切片的内容。
- 内存地址:
arrayA
和arrayB
指向相同的底层数组,但它们的切片结构体(包含指针、长度和容量)在内存中是不同的。 - 修改影响:修改
arrayB
的元素直接影响arrayA
,因为它们共享相同的底层数组。 - 这种机制使得 Go 切片既灵活又高效,能够轻松共享和修改数据。
在 Go 语言中,传递数据到函数时,使用数组和切片的内存管理方式有显著的区别。通常情况下,使用切片作为参数会更加高效,因为切片是引用类型,仅包含指向底层数组的指针、长度和容量,而不需要复制整个数组。
数组与切片的内存消耗
- 数组传递: 当将一个大数组(如 array 函数中的 [1024]int)传递给函数时,Go 会复制整个数组。这意味着如果数组很大,这种复制会消耗大量内存,并且可能导致性能下降。
- 切片传递: 切片则不同。当我们传递切片时,实际上是传递一个结构体,其中包含指向底层数组的指针。这样,不论切片的大小如何,内存消耗都是固定的,因为只有指针、长度和容量被复制,不需要额外的内存。
存在反例
尽管切片在大多数情况下更高效,但仍有一些场景需要注意:
- 小数组和切片的比较: 在一些情况下,尤其是当数组很小且频繁创建时,复制小数组的开销可能与切片相近,甚至可能更低。因此,使用数组的简单性在这些情况下可能更具优势。
- 内存分配: 使用 make 创建切片时,会分配内存,而使用数组则是在栈上分配内存。在极少数情况下,栈内存的使用可能更高效,尤其是在大数组被复制时。
小结
- 切片的效率:切片的优势在于它们通过引用传递避免了大内存块的复制,通常比数组更高效。
- 场景选择:在选择使用数组或切片时,需要考虑数据的大小、内存开销和性能需求。在大多数情况下,切片是更优选择,但在特定场景中,简单的数组可能会更高效。
测试
go
package main
import "testing"
func array() [1024]int {
var x [1024]int
for i := 0; i < len(x); i++ {
x[i] = i
}
return x
}
func slice() []int {
x := make([]int, 1024)
for i := 0; i < len(x); i++ {
x[i] = i
}
return x
}
func BenchmarkArray(b *testing.B) {
for i := 0; i < b.N; i++ {
array()
}
}
func BenchmarkSlice(b *testing.B) {
for i := 0; i < b.N; i++ {
slice()
}
}
(1)Go 的测试工具要求测试文件名以 _test.go 结尾。如果你的文件名是 main.go,那么你需要将其重命名为 main_test.go 或创建一个新的测试文件 main_test.go。
(2)确保你的测试文件导入了 testing 包。
(3)运行命令 go test -bench . -benchmem -gcflags "-N -l"
,确保你在包含 *_test.go 文件的目录中。禁用内联和优化,来观察切片的堆上内存分配的情况。
- 测试 Array 的时候,用的是12核,循环次数是411032,平均每次执行时间是2796 ns,每次执行堆上分配内存总量是0,分配次数也是0 。
- 而切片的结果就"差"一点,同样也是用的是12核,循环次数是162054,平均每次执行时间是6436ns,但是每次执行一次,堆上分配内存总量是8192,分配次数也是1 。
- 这样对比看来,并非所有时候都适合用切片代替数组,因为切片底层数组可能会在堆上分配内存,而且小数组在栈上拷贝的消耗也未必比make 消耗大。
切片的数据结构
切片在 Go 中实际上是一个结构体,通常包含以下三个字段:
- 指针(ptr):指向底层数组的首地址。
- 长度(len):切片当前包含的元素数量。
- 容量(cap):底层数组的总元素数量,从切片的起始位置到数组末尾的元素数量。
go
type Slice struct {
ptr *int // 指向底层数组的指针
len int // 当前长度
cap int // 容量
}
切片本身并不是动态数组或者数组指针。它内部实现的数据结构通过指针引用底层数组,设定相关属性将数据读写操作限定在指定的区域内。切片本身是一个只读对象,其工作机制类似数组指针的一种封装。
切片(slice)是对数组一个连续片段的引用,所以切片是一个引用类型(因此更类似于 C/C++ 中的数组类型,或者 Python 中的 list 类型)。这个片段可以是整个数组,或者是由起始和终止索引标识的一些项的子集。需要注意的是,终止索引标识的项不包括在切片内。切片提供了一个与指向数组的动态窗口。
给定项的切片索引可能比相关数组的相同元素的索引小。和数组不同的是,切片的长度可以在运行时修改 ,最小为 0 最大为相关数组的长度:切片是一个长度可变的数组。
如果想从 slice 中得到一块内存地址,可以这样做:
go
s := make([]byte, 200)
ptr := unsafe.Pointer(&s[0])
如果反过来呢?从 Go 的内存地址中构造一个 slice。
c
var ptr unsafe.Pointer
var s1 = struct {
addr uintptr
len int
cap int
}{ptr, length, length}
s := *(*[]byte)(unsafe.Pointer(&s1))
更加直接的方法,在 Go 的反射中就存在一个与之对应的数据结构 SliceHeader,我们可以用它来构造一个 slice:
go
var o []byte
sliceHeader := (*reflect.SliceHeader)((unsafe.Pointer(&o)))
sliceHeader.Cap = length
sliceHeader.Len = length
sliceHeader.Data = uintptr(ptr)
创建切片
make 函数允许在运行期动态指定数组长度,绕开了数组类型必须使用编译期常量的限制。
创建切片有两种形式,make 创建切片,空切片。
go
func makeslice(et *_type, len, cap int) slice {
// 根据切片的数据类型,获取切片的最大容量
maxElements := maxSliceCap(et.size)
// 比较切片的长度,长度值域应该在[0,maxElements]之间
if len < 0 || uintptr(len) > maxElements {
panic(errorString("makeslice: len out of range"))
}
// 比较切片的容量,容量值域应该在[len,maxElements]之间
if cap < len || uintptr(cap) > maxElements {
panic(errorString("makeslice: cap out of range"))
}
// 根据切片的容量申请内存
p := mallocgc(et.size*uintptr(cap), et, true)
// 返回申请好内存的切片的首地址
return slice{p, len, cap}
}
c
func makeslice64(et *_type, len64, cap64 int64) slice {
len := int(len64)
if int64(len) != len64 {
panic(errorString("makeslice: len out of range"))
}
cap := int(cap64)
if int64(cap) != cap64 {
panic(errorString("makeslice: cap out of range"))
}
return makeslice(et, len, cap)
}
除了 make 函数可以创建切片以外,字面量也可以创建切片。
这里是用字面量创建的一个 len = 6,cap = 6 的切片,这时候数组里面每个元素的值都初始化完成了。需要注意的是 [ ] 里面不要写数组的容量,因为如果写了个数以后就是数组了,而不是切片了。
上图就 Slice A 创建出了一个 len = 3,cap = 3 的切片。从原数组的第二位元素(0是第一位)开始切,一直切到第四位为止(不包括第五位)。同理,Slice B 创建出了一个 len = 2,cap = 4 的切片。
nil 和空切片
go
var slice []int
nil 切片被用在很多标准库和内置函数中,描述一个不存在的切片的时候,就需要用到 nil 切片。比如函数在发生异常的时候,返回的切片就是 nil 切片。nil 切片的指针指向 nil。
空切片一般会用来表示一个空的集合。比如数据库查询,一条结果也没有查到,那么就可以返回一个空切片。
go
silce := make( []int , 0 )
slice := []int{ }
空切片和 nil 切片的区别在于,空切片指向的地址不是nil,指向的是一个内存地址,但是它没有分配任何内存空间,即底层元素包含0个元素。
最后需要说明的一点是。不管是使用 nil 切片还是空切片,对其调用内置函数 append,len 和 cap 的效果都是一样的。
切片扩容
c
func growslice(et *_type, old slice, cap int) slice {
if raceenabled {
callerpc := getcallerpc(unsafe.Pointer(&et))
racereadrangepc(old.array, uintptr(old.len*int(et.size)), callerpc, funcPC(growslice))
}
if msanenabled {
msanread(old.array, uintptr(old.len*int(et.size)))
}
if et.size == 0 {
// 如果新要扩容的容量比原来的容量还要小,这代表要缩容了,那么可以直接报panic了。
if cap < old.cap {
panic(errorString("growslice: cap out of range"))
}
// 如果当前切片的大小为0,还调用了扩容方法,那么就新生成一个新的容量的切片返回。
return slice{unsafe.Pointer(&zerobase), old.len, cap}
}
// 这里就是扩容的策略
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
newcap = cap
} else {
if old.len < 1024 {
newcap = doublecap
} else {
for newcap < cap {
newcap += newcap / 4
}
}
}
// 计算新的切片的容量,长度。
var lenmem, newlenmem, capmem uintptr
const ptrSize = unsafe.Sizeof((*byte)(nil))
switch et.size {
case 1:
lenmem = uintptr(old.len)
newlenmem = uintptr(cap)
capmem = roundupsize(uintptr(newcap))
newcap = int(capmem)
case ptrSize:
lenmem = uintptr(old.len) * ptrSize
newlenmem = uintptr(cap) * ptrSize
capmem = roundupsize(uintptr(newcap) * ptrSize)
newcap = int(capmem / ptrSize)
default:
lenmem = uintptr(old.len) * et.size
newlenmem = uintptr(cap) * et.size
capmem = roundupsize(uintptr(newcap) * et.size)
newcap = int(capmem / et.size)
}
// 判断非法的值,保证容量是在增加,并且容量不超过最大容量
if cap < old.cap || uintptr(newcap) > maxSliceCap(et.size) {
panic(errorString("growslice: cap out of range"))
}
var p unsafe.Pointer
if et.kind&kindNoPointers != 0 {
// 在老的切片后面继续扩充容量
p = mallocgc(capmem, nil, false)
// 将 lenmem 这个多个 bytes 从 old.array地址 拷贝到 p 的地址处
memmove(p, old.array, lenmem)
// 先将 P 地址加上新的容量得到新切片容量的地址,然后将新切片容量地址后面的 capmem-newlenmem 个 bytes 这块内存初始化。为之后继续 append() 操作腾出空间。
memclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem)
} else {
// 重新申请新的数组给新切片
// 重新申请 capmen 这个大的内存地址,并且初始化为0值
p = mallocgc(capmem, et, true)
if !writeBarrier.enabled {
// 如果还不能打开写锁,那么只能把 lenmem 大小的 bytes 字节从 old.array 拷贝到 p 的地址处
memmove(p, old.array, lenmem)
} else {
// 循环拷贝老的切片的值
for i := uintptr(0); i < lenmem; i += et.size {
typedmemmove(et, add(p, i), add(old.array, i))
}
}
}
// 返回最终新切片,容量更新为最新扩容之后的容量
return slice{p, old.len, newcap}
}
上述就是扩容的实现。主要需要关注的有两点,一个是扩容时候的策略,还有一个就是扩容是生成全新的内存地址还是在原来的地址后追加。
扩容策略
go
func main() {
slice := []int{10, 20, 30, 40}
newSlice := append(slice, 50)
fmt.Printf("Before slice = %v, Pointer = %p, len = %d, cap = %d\n", slice, &slice, len(slice), cap(slice))
fmt.Printf("Before newSlice = %v, Pointer = %p, len = %d, cap = %d\n", newSlice, &newSlice, len(newSlice), cap(newSlice))
newSlice[1] += 10
fmt.Printf("After slice = %v, Pointer = %p, len = %d, cap = %d\n", slice, &slice, len(slice), cap(slice))
fmt.Printf("After newSlice = %v, Pointer = %p, len = %d, cap = %d\n", newSlice, &newSlice, len(newSlice), cap(newSlice))
}
/*
Before slice = [10 20 30 40], Pointer = 0xc0000081b0, len = 4, cap = 4
Before newSlice = [10 20 30 40 50], Pointer = 0xc0000081c8, len = 5, cap = 8
After slice = [10 20 30 40], Pointer = 0xc0000081b0, len = 4, cap = 4
After newSlice = [10 30 30 40 50], Pointer = 0xc0000081c8, len = 5, cap = 8
*/
新的切片和之前的切片已经不同了,因为新的切片更改了一个值,并没有影响到原来的数组,新切片指向的数组是一个全新的数组。并且 cap 容量也发生了变化。这之间究竟发生了什么呢?
Go 中切片扩容的策略是这样的:
如果切片的容量小于 1024 个元素,于是扩容的时候就翻倍增加容量。上面那个例子也验证了这一情况,总容量从原来的4个翻倍到现在的8个。
一旦元素个数超过 1024 个元素,那么增长因子就变成 1.25 ,即每次增加原来容量的四分之一。
注意:扩容扩大的容量都是针对原来的容量而言的,而不是针对原来数组的长度而言的。
新数组 or 老数组 ?
再谈谈扩容之后的数组一定是新的么?这个不一定,分两种情况。
情况一:
go
func main() {
array := [4]int{10, 20, 30, 40}
slice := array[0:2]
newSlice := append(slice, 50)
fmt.Printf("Before slice = %v, Pointer = %p, len = %d, cap = %d\n", slice, &slice, len(slice), cap(slice))
fmt.Printf("Before newSlice = %v, Pointer = %p, len = %d, cap = %d\n", newSlice, &newSlice, len(newSlice), cap(newSlice))
newSlice[1] += 10
fmt.Printf("After slice = %v, Pointer = %p, len = %d, cap = %d\n", slice, &slice, len(slice), cap(slice))
fmt.Printf("After newSlice = %v, Pointer = %p, len = %d, cap = %d\n", newSlice, &newSlice, len(newSlice), cap(newSlice))
fmt.Printf("After array = %v\n", array)
}
/*
Before slice = [10 20], Pointer = 0xc000092198, len = 2, cap = 4
Before newSlice = [10 20 50], Pointer = 0xc0000921b0, len = 3, cap = 4
After slice = [10 30], Pointer = 0xc000092198, len = 2, cap = 4
After newSlice = [10 30 50], Pointer = 0xc0000921b0, len = 3, cap = 4
After array = [10 30 50 40]
*/
在这种情况下,扩容以后并没有新建一个新的数组,扩容前后的数组都是同一个,这也就导致了新的切片修改了一个值,也影响到了老的切片了。并且 append() 操作也改变了原来数组里面的值。一个 append() 操作影响了这么多地方,如果原数组上有多个切片,那么这些切片都会被影响!无意间就产生了莫名的 bug!
这种情况,由于原数组还有容量可以扩容,所以执行 append() 操作以后,会在原数组上直接操作,所以这种情况下,扩容以后的数组还是指向原来的数组。
这种情况也极容易出现在字面量创建切片时候,第三个参数 cap 传值的时候,如果用字面量创建切片,cap 并不等于指向数组的总容量,那么这种情况就会发生。
go
slice := array[1:2:3]
上面这种情况非常危险,极度容易产生 bug 。
建议用字面量创建切片的时候,cap 的值一定要保持清醒,避免共享原数组导致的 bug。
情况二其实就是在扩容策略里面举的例子,在那个例子中之所以生成了新的切片,是因为原来数组的容量已经达到了最大值,再想扩容, Go 默认会先开一片内存区域,把原来的值拷贝过来,然后再执行 append() 操作。这种情况丝毫不影响原数组。
所以建议尽量避免情况一,尽量使用情况二,避免 bug 产生。
切片拷贝
Slice 中拷贝方法有2个:
(1)slicecopy
go
func slicecopy(to, fm slice, width uintptr) int {
// 如果源切片或者目标切片有一个长度为0,那么就不需要拷贝,直接 return
if fm.len == 0 || to.len == 0 {
return 0
}
// n 记录下源切片或者目标切片较短的那一个的长度
n := fm.len
if to.len < n {
n = to.len
}
// 如果入参 width = 0,也不需要拷贝了,返回较短的切片的长度
if width == 0 {
return n
}
// 如果开启了竞争检测
if raceenabled {
callerpc := getcallerpc(unsafe.Pointer(&to))
pc := funcPC(slicecopy)
racewriterangepc(to.array, uintptr(n*int(width)), callerpc, pc)
racereadrangepc(fm.array, uintptr(n*int(width)), callerpc, pc)
}
// 如果开启了 The memory sanitizer (msan)
if msanenabled {
msanwrite(to.array, uintptr(n*int(width)))
msanread(fm.array, uintptr(n*int(width)))
}
size := uintptr(n) * width
if size == 1 {
// TODO: is this still worth it with new memmove impl?
// 如果只有一个元素,那么指针直接转换即可
*(*byte)(to.array) = *(*byte)(fm.array) // known to be a byte pointer
} else {
// 如果不止一个元素,那么就把 size 个 bytes 从 fm.array 地址开始,拷贝到 to.array 地址之后
memmove(to.array, fm.array, size)
}
return n
}
slicecopy 方法会把源切片值(即 fm Slice )中的元素复制到目标切片(即 to Slice )中,并返回被复制的元素个数,copy 的两个类型必须一致。slicecopy 方法最终的复制结果取决于较短的那个切片,当较短的切片复制完成,整个复制过程就全部完成了。
go
package main
import "fmt"
func main() {
array := []int{10, 20, 30, 40}
slice := make([]int, 6)
n := copy(slice, array)
fmt.Println(n, slice, array) // 4 [10 20 30 40 0 0] [10 20 30 40]
}
(2)slicestringcopy
go
func slicestringcopy(to []byte, fm string) int {
// 如果源切片或者目标切片有一个长度为0,那么就不需要拷贝,直接 return
if len(fm) == 0 || len(to) == 0 {
return 0
}
// n 记录下源切片或者目标切片较短的那一个的长度
n := len(fm)
if len(to) < n {
n = len(to)
}
// 如果开启了竞争检测
if raceenabled {
callerpc := getcallerpc(unsafe.Pointer(&to))
pc := funcPC(slicestringcopy)
racewriterangepc(unsafe.Pointer(&to[0]), uintptr(n), callerpc, pc)
}
// 如果开启了 The memory sanitizer (msan)
if msanenabled {
msanwrite(unsafe.Pointer(&to[0]), uintptr(n))
}
// 拷贝字符串至字节数组
memmove(unsafe.Pointer(&to[0]), stringStructOf(&fm).str, uintptr(n))
return n
}
go
package main
import "fmt"
func main() {
slice := make([]byte, 3)
n := copy(slice, "abcdef")
fmt.Println(n, slice) // 3 [97 98 99]
}
切片遍历中的 range
range 是 Go 用于遍历数组、切片、映射和通道的关键字。在切片中,range 遍历时会产生两个值:
- 索引值:当前遍历到的元素索引。
- 元素值:当前索引处的元素值的拷贝。
range 的值是拷贝
在使用 range 遍历切片时,得到的是切片中每个元素的值拷贝,而不是元素的引用。这意味着对遍历得到的值进行修改,不会影响原切片。
go
package main
import "fmt"
func main() {
slice := []int{1, 2, 3, 4}
// 遍历并尝试修改值
for _, v := range slice {
v = v * 10
}
fmt.Println(slice) // 输出:[1 2 3 4],原切片未改变
}
需要通过索引来访问切片中的元素进行修改:
java
package main
import "fmt"
func main() {
slice := []int{1, 2, 3, 4}
// 通过索引修改原切片的值
for i := range slice {
slice[i] = slice[i] * 10
}
fmt.Println(slice) // 输出:[10 20 30 40],原切片已改变
}
引用
- https://www.runoob.com/go/go-tutorial.html
- https://www.topgoer.cn/
- https://gopl-zh.github.io
- https://www.jianshu.com/p/030aba2bff41