数组与切片
数组
定义数组需要在定义时指定数组的长度和存放的数据类型
数组初始化成功后,存储类型和长度不能改变
如果要存储更多的元素,需要先创建更长的数组,然后将原数组的数据拷贝到新数组中
切片
切片属于引用类型,go语言中引用类型包括slice,map,channel,函数和指针
引用类型在赋值时拷贝的是指针的值,也就是说拷贝后新变量和旧变量的值(即指向的地址)是相同的,这点和C语言一样
切片的内部实现
切片的底层实现是一个数据结构,可以按照需要动态的自动增长和缩小
切片的动态增长是通过append函数来实现的,动态缩小是通过对切片进行切片来实现
注意切片在内存中也是连续分配的,所以对于切片也能够获取索引和进行迭代
源码定义src/runtime/slice.go,包括了切片的指针,当前的长度和当前的容量
type slice struct {
array unsafe.Pointer
len int
cap int
}
字符串
字符的实现
字符串和字符数组不同。
字符串一旦确定之后,其中的元素就不可被更改了,长度当然也是固定的
type stringStruct struct {
array unsafe.Pointer // 指向字符串首地址
length int // 长度
}
字符串结构体包括两个部分,一个是指向底层的字节数组的指针;一个是字符串长度的变量
字符串不是切片,但是可以通过切片操作
字符串的修改
字符串作为常量不可以直接修改,只能通过先转换成[]byte数组,再转换成string来完成
但是这个过程一定会重新分配内存
func main() {
s1 := "hello"
b1 := []byte(s1)
b1[0] = 'a'
s2 := string(b1)
fmt.Println(s2) //aello
}
channel
channel实现
type hchan struct {
buf unsafe.Pointer // points to an array of dataqsiz elements
sendx uint // send index
recvx uint // receive index
recvq waitq // list of recv waiters
sendq waitq // list of send waiters
lock mutex
}
- buf,循环链表
- sendx,recvx用于记录buf循环链表中发送者和接受者的index
- lock, 互斥锁
- recvq,sendq,双向链表,分别是接收者和发送者goroutine的抽象出来的结构体队列
创建channel
创建channel实际上是创建一个hchan结构体,然后返回指针
函数的调用传递就是传递的这个指针,因此不需要使用channel的指针,直接使用channel即可
未初始化channel写
func main() {
var c chan int //只声明,没有初始化
fmt.Printf("%v", c) //<nil>
c <- 1
}
// <nil>fatal error: all goroutines are asleep - deadlock!
//goroutine 1 [chan send (nil chan)]:
未初始化channel读
func main() {
var c chan int //只声明,没有初始化
fmt.Printf("%v", c) //<nil>
r := <-c
fmt.Println(r)
}
//<nil>fatal error: all goroutines are asleep - deadlock!
//goroutine 1 [chan receive (nil chan)]:
未初始化channel关闭
func main() {
var c chan int //只声明,没有初始化
fmt.Printf("%v", c) //<nil>
close(c)
}
//<nil>panic: close of nil channel
对关闭的channel进行读写
- 对关闭的channel进行写会导致panic
func main() {
var c chan int = make(chan int, 5)
close(c)
c <- 1
}
//panic: send on closed channel
- 对关闭的channel进行写读分两种情况:管道关闭时还有元素,和没有元素了
func main() {
var c chan int = make(chan int, 5)
c <- 1
c <- 2
close(c)
r, ok := <-c
fmt.Printf("%v, %v\n", r, ok)
r, ok = <-c
fmt.Printf("%v, %v\n", r, ok)
r, ok = <-c
fmt.Printf("%v, %v\n", r, ok)
}
//1, true
//2, true
//0, false
内存逃逸
在函数中申请的临时变量会被存放在栈中,如下
func f(){
t := make([]int, 5)
}
但是如果将申请的临时变量作为返回值返回,编译器会认为在退出函数之后还有其他地方引用,这是会在堆里进行申请,如下
func f() []int{
t := make([]int, 5)
...
return t
}
分配到堆中,栈不会自动清理,并且会引起Go的垃圾回收机制(占用较大的开销)
编译器会根据变量是否被外部引用来决定是否逃逸。如果函数外部没有引用,优先放到栈中;否则必定放到堆中
编译器决定内存分配位置的方式,就称之为逃逸分析(escape analysis)。逃逸分析由编译器完成,作用于编译阶段。
注意go是在编译阶段确定的逃逸,而不是在运行时
逃逸的几种情况
- 情况1,指针逃逸
在函数中创建了一个对象,返回了这个对象的指针。这种情况下,函数虽然退出了,但是因为指针的存在,对象的内存不能随着函数结束而回收,因此只能分配在堆上。
func f() []int {
arr := []int{1, 2}
return arr
}
func main() {
r := f()
fmt.Println(r)
}
PS E:\go-work> go build -gcflags=-m .\lab3.go
# command-line-arguments
.\lab3.go:5:6: can inline f
.\lab3.go:10:8: inlining call to f
.\lab3.go:11:13: inlining call to fmt.Println
.\lab3.go:6:14: []int{...} escapes to heap
.\lab3.go:10:8: []int{...} escapes to heap
.\lab3.go:11:13: ... argument does not escape
.\lab3.go:11:14: r escapes to heap
- 情况2,interface{}动态类型逃逸
空接口即 interface{} 可以表示任意的类型,如果函数参数为 interface{},编译期间很难确定其参数的具体类型,也会发生逃逸。
比如fmt.Println函数参数
func Println(a ...interface{}) (n int, err error)
PS E:\go-work> go build -gcflags=-m .\lab3.go
# command-line-arguments
.\lab3.go:5:6: can inline main
.\lab3.go:6:13: inlining call to fmt.Println
.\lab3.go:6:13: ... argument does not escape
.\lab3.go:6:14: "hello,123" escapes to heap
- 情况3,栈空间不足
对Go编译器而言,超过一定大小的局部变量将逃逸到堆上,不同的Go版本的大小限制可能不一样
- 情况4,闭包
闭包访问的函数内的外部变量也会被分配(逃逸)到堆上
内存逃逸总结
堆上分配内存比栈上分配内存开销大
不要盲目使用变量的指针做为函数的返回值,虽然会减少复制操作,但是内存的回收会造成更大的开销
内存泄漏
由于疏忽或错误造成程序未能释放已经不再使用的内存
Go内存泄漏有两种情况
- 僵尸goroutine中分配的内存对象无法被回收
- 全局数据(生命周期和程序运行周期一样)引用了本应该被释放的对象
goroutine泄漏的场景
- 情况1,从channel里读数据,但是没有写操作
如下leak函数中的goroutine会被锁死
func leak() {
ch := make(chan int)
go func() {
val := <-ch
fmt.Println("received value:", val)
}()
}
- 情况2,向已满的channel写数据,但是没有读操作
- 情况3,select操作在所有case上阻塞
比如没有对channel进行close操作
func fibonacci(c, quit chan int) {
x, y := 0, 1
for{
select {
case c <- x:
x, y = y, x+y
case <-quit:
fmt.Println("quit")
return
}
}
}
func main() {
c := make(chan int)
quit := make(chan int)
go fibonacci(c, quit)
for i := 0; i < 10; i++{
fmt.Println(<- c)
}
// close(quit)
}
- 情况5,goroutine死循环
func f(){
for{
//...
}
}