1. 《Go语言四十二章经》第二十四章 指针和内存
作者:李骁
1.1. 24.1 指针
一个指针变量可以指向任何一个值的内存地址。它指向那个值的内存地址,在 32 位机器上占用 4 个字节,在 64 位机器上占用 8 个字节,并且与它所指向的值的大小无关。当然,可以声明指针指向任何类型的值来表明它的原始性或结构性;你可以在指针类型前面加上*号(前缀)来获取指针所指向的内容,这里的*号是一个类型更改器。使用一个指针引用一个值被称为间接引用。
当一个指针被定义后没有分配到任何变量时,它的值为 nil。
一个指针变量通常缩写为 ptr。
符号 "*" 可以放在一个指针前,如 “*intP”,那么它将得到这个指针指向地址上所存储的值;这被称为反引用(或者内容或者间接引用)操作符;另一种说法是指针转移。
对于任何一个变量 var, 如下表达式都是正确的:var == *(&var)
注意事项:
你不能得到一个数字或常量的地址,下面的写法是错误的。
例如:
const i = 5
ptr := &i // error: cannot take the address of i
ptr2 := &10 // error: cannot take the address of 10
所以说,Go 语言和 C、C++ 以及 D 语言这些低级(系统)语言一样,都有指针的概念。
但是对于经常导致 C 语言内存泄漏继而程序崩溃的指针运算(所谓的指针算法,如:pointer+2,移动指针指向字符串的字节数或数组的某个位置)是不被允许的。
Go 语言中的指针保证了内存安全,更像是 Java、C# 和 VB.NET 中的引用。
因此 c = *p++ 在 Go 语言的代码中是不合法的。
指针的一个高级应用是你可以传递一个变量的引用(如函数的参数),这样不会传递变量的拷贝。指针传递是很廉价的,只占用 4 个或 8 个字节。当程序在工作中需要占用大量的内存,或很多变量,或者两者都有,使用指针会减少内存占用和提高效率。被指向的变量也保存在内存中,直到没有任何指针指向它们,所以从它们被创建开始就具有相互独立的生命周期。
另一方面(虽然不太可能),由于一个指针导致的间接引用(一个进程执行了另一个地址),指针的过度频繁使用也会导致性能下降。
指针也可以指向另一个指针,并且可以进行任意深度的嵌套,导致你可以有多级的间接引用,但在大多数情况这会使你的代码结构不清晰。
如我们所见,在大多数情况下 Go 语言可以使程序员轻松创建指针,并且隐藏间接引用,如:自动反向引用。
对一个空指针的反向引用是不合法的,并且会使程序崩溃:
package main
func main() {
var p *int = nil
*p = 0
}
panic: runtime error: invalid memory address or nil pointer dereference
指针的使用方法:
定义指针变量;
为指针变量赋值;
访问指针变量中指向地址的值;
在指针类型前面加上*号来获取指针所指向的内容。
package main
import "fmt"
func main() {
var a, b int = 20, 30 // 声明实际变量
var ptra *int // 声明指针变量
var ptrb *int = &b
ptra = &a // 指针变量的存储地址
fmt.Printf("a 变量的地址是: %x\n", &a)
fmt.Printf("b 变量的地址是: %x\n", &b)
// 指针变量的存储地址
fmt.Printf("ptra 变量的存储地址: %x\n", ptra)
fmt.Printf("ptrb 变量的存储地址: %x\n", ptrb)
// 使用指针访问值
fmt.Printf("*ptra 变量的值: %d\n", *ptra)
fmt.Printf("*ptrb 变量的值: %d\n", *ptrb)
}
1.2. 24.2 new() 和 make() 的区别
看起来二者没有什么区别,都在堆上分配内存,但是它们的行为不同,适用于不同的类型。
new(T) 为每个新的类型T分配一片内存,初始化为 0 并且返回类型*T的内存地址:这种方法 返回一个指向类型为 T,值为 0 的地址的指针,它适用于值类型如数组和结构体;它相当于 &T{}。
make(T) 返回一个类型为 T 的初始值,它只适用于3种内建的引用类型:切片、map 和 channel。
你并不总是知道变量是分配到栈还是堆上。在C++中,使用new创建的变量总是在堆上。在Go中,即使是使用 new() 或者 make() 函数来分配,变量的位置还是由编译器决定。编译器根据变量的大小和泄露分析的结果来决定其位置。这也意味着在局部变量上返回引用是没问题的,而这在C或者C++这样的语言中是不行的。
如果你想知道变量分配的位置,在"go build"或"go run"上传入"-m" "-gcflags"(即,go run -gcflags -m app.go)。
go run -gcflags -m main.go
# command-line-arguments
.\main.go:12:31: m.Alloc / 1024 escapes to heap
.\main.go:11:23: main &m does not escape
.\main.go:12:12: main ... argument does not escape
1.3. 24.3 垃圾回收和 SetFinalizer
Go 语言开发者一般不需要写代码来释放不再使用的变量或结构体占用的内存,在 Go语言运行时有一个独立的进程,即垃圾收集器(GC),会专门处理这些事情,它搜索不再使用的变量然后释放它们占用的内存,这是自动垃圾回收;还有一种是主动垃圾回收,通过显式调用 runtime.GC()来实现。
通过调用 runtime.GC() 函数可以显式的触发 GC,这在某些的场景下非常有用,比如当内存资源不足时调用 runtime.GC(),它会在此函数执行的点上立即释放一大片内存,但此时程序可能会有短时的性能下降(因为 GC 进程在执行)。
下面代码中的func (p *Person) NewOpen()在某些情况下非常有必要这样处理,比如某些资源占用申请,开发人员可能忘记使用defer Close()来销毁处理,但通过SetFinalizer,如果GC自动运行或者手动运行GC,则都能及时销毁这些资源,释放占用的内存而避免内存泄漏。
GC过程中重要的函数func SetFinalizer(obj interface{}, finalizer interface{})有两个参数,参数一:obj必须是指针类型。参数二:finalizer是一个函数,其参数类型是obj的类型,其没有返回值。
package main
import (
"log"
"runtime"
"time"
)
type Person struct {
Name string
Age int
}
func (p *Person) Close() {
p.Name = "NewName"
log.Println(p)
log.Println("Close")
}
func (p *Person) NewOpen() {
log.Println("Init")
runtime.SetFinalizer(p, (*Person).Close)
}
func Tt(p *Person) {
p.Name = "NewName"
log.Println(p)
log.Println("Tt")
}
// 查看内存情况
func Mem(m *runtime.MemStats) {
runtime.ReadMemStats(m)
log.Printf("%d Kb\n", m.Alloc/1024)
}
func main() {
var m runtime.MemStats
Mem(&m)
var p *Person = &Person{Name: "lee", Age: 4}
p.NewOpen()
log.Println("Gc完成第一次")
log.Println("p:", p)
runtime.GC()
time.Sleep(time.Second * 5)
Mem(&m)
var p1 *Person = &Person{Name: "Goo", Age: 9}
runtime.SetFinalizer(p1, Tt)
log.Println("Gc完成第二次")
time.Sleep(time.Second * 2)
runtime.GC()
time.Sleep(time.Second * 2)
Mem(&m)
}
本书《Go语言四十二章经》内容在github上同步地址:https://github.com/ffhelicopter/Go42 本书《Go语言四十二章经》内容在简书同步地址: https://www.jianshu.com/nb/29056963
虽然本书中例子都经过实际运行,但难免出现错误和不足之处,烦请您指出;如有建议也欢迎交流。 联系邮箱:roteman@163.com