golang 指针操作

golang pointer operation

foreversmart write on 2022-01-19
在 go 语言中每个变量都有自己的地址,记录变量地址的类型称为指针类型
& 获取变量的地址,生成一个 *T 的指针类型指向取值变量 x
x 必须是可以寻址的
* 获取指针指向的值
如果指针是 nil 会 panic
如果变量不是指针类型会报编译错误 invalid operation: cannot indirect

可寻址(Addressable

可寻址是指可以通过 & 获取变量的地址
和可寻址对应的就是不可寻址 unaddressable golang 中除了不可寻址的变量其余的变量就是可寻址的变量
Go
Copy
a variable, pointer indirection, or slice indexing operation; or a field selector of an addressable struct operand; or an array indexing operation of an addressable array
不可寻址的类型包括:
Map value:因为 Golang 中的 Map 里面 Value 的地址是会按需进行变化的,其次如果 key 不存在会返回零值,零值不可寻址。
Go
Copy
c := &m["key"] // invalid operation: cannot take address of m["key"]
Const 常量:如果可寻址,则可以通过指针修改值破坏了常量的不可变性
字符串中的字节:和常量类似,golang 中的字符串也是不可变的
字面量 literal, T{} 字面量不可寻址,但是 &T{} 是可以正常使用的因为这是一个语法糖 &{} 是 tmp := T{}; (&tmp) 的简写
直接对操作结果取地址,除了指针操作其它都不能获取地址
channle 取值操作
Go
Copy
var a = 1 ap := &a &*ap // ok c := make(chan int, 20) &<-c // invalid operation: cannot take address
子字符串,子 slice 操作
Go
Copy
s := "hello" &s[0:3] // invalid operation: cannot take address slice := make([]int,0, 5) &slice[0:3 // invalid operation: cannot take address arr := []int{1, 2, 3, 4, 5} &arr[0:3] // invalid operation: cannot take address
+-*/%等等操作
Go
Copy
b := &+a // invalid operation: cannot take address b := &(a + 1) // invalid operation: cannot take address b := &(a - 1) // invalid operation: cannot take address b := &(a / 2) // invalid operation: cannot take address b := &(a * 2) // invalid operation: cannot take address b := &(a % 1) // invalid operation: cannot take address
显示类型转换操作和类型断言操作(通过type assertions获取接口的动态类型值)
Go
Copy
b := &int64(a) // invalid operation: cannot take address var i interface{} i = a c := &i.(int) // invalid operation: cannot take address
Function and Method 不能直接操作取值获取返回值的地址, 即使返回的是指针类型也不行
Go
Copy
func hello() string { return "hello" } func helloPointer() *string { h := "hello" return &h } func (a *A) method() string { return "hello" } c := &hello() // invalid operation: cannot take address of hello() c := &a.method() // invalid operation: cannot take address of a.method() c := &helloPointer() // invalid operation: cannot take address of helloPointer()
对于返回值是 struct , array ,map 类型的时候也不能直接获取他们的字段或者调用 index 获取 value
Go
Copy
func Map() map[string]string { res := make(map[string]string) return res } func Array() [5]int { //res := make([]int, 0, 5) return [5]int{1, 1, 1, 1, 1} } func Struct() A { return A{} } c := &Map()["1"] // invalid operation: cannot take address of Map()["1"] d := &Array()[0] // invalid operation: cannot take address of Array()[0] e := &Struct().B // invalid operation: cannot take address of Struct().B
但是如果将返回的类型变为指针类型则可以做此类操作
Go
Copy
func Slice() []int { res := make([]int, 0, 5) return res } func Struct() *A { return &A{} } d := &Slice()[0] // ok e := &Struct().B // ok

反射中的可寻址

在 Golang 的反射包 reflect 中 有一个方法是 CanAddr() 这个方法对可寻址的范围比前面将的可寻址的范围更小:
CanAddr reports whether the value's address can be obtained with Addr. Such values are called addressable. A value is addressable if it is an element of a slice, an element of an addressable array, a field of an addressable struct, or the result of dereferencing a pointer. If CanAddr returns false, calling Addr will panic.
slice的元素
可寻址数组的元素
可寻址struct的字段
指针引用的结果
单纯的变量基本都是不可寻址的,对于变量为了动态修改他的值我们需要先使用 & 操作生成一个指针类型,再获取这个指针指向的值。Golang 中的反射不会自动的帮我们做获取地址的操作,所以只能对上面四种已经有地址的类型返回具体的地址
Go
Copy
a := 1 c := &a v := reflect.Indirect(reflect.ValueOf(c)) // CanAddr() true v1 := reflect.ValueOf(a) // CanAddr() false

unsafe.Pointer

unsafe.Pointer是特别定义的一种指针类型,它可以包含任意类型变量的地址,但是不能通过 * 操作获取指针指向真实变量的值,因为变量的具体类型是未知的。
其包含四种核心操作:
任何类型的指针值都可以转换为 Pointer
Go
Copy
var ( a int b string ) pa := unsafe.Pointer(&a) pb := unsafe.Pointer(&b)
Pointer 可以转换为任何类型的指针值
Go
Copy
va := *(*string)(pa) vb := *(*int)(pb) // 实际上面这个转换在使用的时候会 panic runtime error: invalid memory address or nil pointer dereference
Pointer 可以转换为 uintptr, uintptr 本质是一个 Int 基本类型但是长度被设计足够存储任何指针的大小。通过转换为 uintptr 我们可以对地址进行数值计算,而 Pointer 类型和golang 中的指针不能进行数值计算。
Go
Copy
p := uintptr(pa) pp := (*uintptr)(pa)
uintptr 可以转换为 Pointer
Go
Copy
x1 := *(*int)(unsafe.Pointer(p)) x2 := *(*int)(unsafe.Pointer(pp)) x1 == x2 // true // INVALID: end points outside allocated space. var s thing end = unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + unsafe.Sizeof(s)) // INVALID: end points outside allocated space. b := make([]byte, n) end = unsafe.Pointer(uintptr(unsafe.Pointer(&b[0])) + uintptr(n))
不在分配的空间外可能会导致错误,如果超出了栈的空间还会报 unexpected fault address 0xc00048ea18 panic
注意在使用的过程中,uintptr 只是记录指针的地址 GC 不会识别到会产生一些错误:
GC 移动变量,由于 uintptr 是基本类型没有被识别到不会被更新,移动后的内存可能不是有效内存地址了导致报错
GC 回收,Pointer 可以看做是指向变量的指针,但是 uintptr 不是。所以有可能 uintptr 还在但是内存已经被回收产生错误
由于上面的两个原因尽量使用 uintptr 时不要使用中间的临时变量来保存,这样 unintptr 的生命周期保持和 Pointer 一致
获取原始变量的地址:
对于变量 a 怎么获取字符串 "hello" 存储的地址呢
Go
Copy
a := "hello" &a // 获取的是 a 的地址
获取 unsafe.Pointer 指向对象的内部结构,其中 data 记录了原始数据的地址
Go
Copy
// a 是 string addra := *(*reflect.StringHeader)(unsafe.Pointer(&a)) // a 是数组 addra := *(*reflect.SliceHeader)(unsafe.Pointer(&a))
unsafe.Pointer 转为 unsafe.Pointer 指针获取 Pointer 指向的地址
Go
Copy
addr := *(*unsafe.Pointer)(unsafe.Pointer(&a))
修改字符串的值:
如果字符串是字面量内存会被分配在常量区 ,如果强行修改会报错。如果通过 []byte 或者 string 转化生成的字符串分配在堆上是可以修改的
Go
Copy
a := "hello" sa := *(*[]byte)(unsafe.Pointer(&a)) sa[0] = '1' // unexpected fault address 0x10a2ffa fatal error: fault ns := string(sa) sn := *(*[]byte)(unsafe.Pointer(&ns)) sa[0] = '1' // ok
指针迷惑性
Go
Copy
// string header type string struct { array unsafe.Pointer // 元素指针 1字节 len int // 长度 1字节 }
想要真正获取 string 底层数组的地址
Go
Copy
addra := *(*reflect.StringHeader)(unsafe.Pointer(&a)) addra.Data // addr a
对于字符串变量 a,对 a 取地址 &a 其实返回的是 a 的地址并不是 string 真正的地址 a
b := &a 和 b := a 两个操作
Go
Copy
b := &a // 等价于 b := string { array: &a len: empty } b := a // 等价于 b := string { array: a.array len: a.len } // 可以通过前面查看真实的地址来进行验证
修改 addra 的 Data 值修改为 为 addrb 的 Data 可以发现一些迷惑性的地方
Go
Copy
a := "hello" b := "test" sha := *(*reflect.StringHeader)(unsafe.Pointer(&a)) sha1 := (*reflect.StringHeader)(unsafe.Pointer(&a)) shb := *(*reflect.StringHeader)(unsafe.Pointer(&b)) sha.Data = shb.Data sha1 := *(*reflect.StringHeader)(unsafe.Pointer(&a)) fmt.Println(sha1, a) // not change sha1.Data = shb.Data sha1 := *(*reflect.StringHeader)(unsafe.Pointer(&a)) fmt.Println(sha1, a) // change
这个是因为 v = * 会的得到一个值对象拷贝给 v 所以导致了修改不了原值
下面是一个简化一点的例子
Go
Copy
i := 1 v := *&i // 发生值拷贝,修改 v 已经不是原值 p := &i fmt.Println(v) // 1 v = 2 fmt.Println(i) // 1 not change *p = 3 // 获取原值,在原值上修改 fmt.Println(i) // 3 change np := *p // 发生值拷贝 和 v := *&i np = 8 // not change fmt.Println(i, np) // 3 8

Receiver

对于指针接受者 *T 可以调用的方法是普通类型 T 的超集,
首先 *T 可以调用所有 T 类型上的方法,因为 *T 类型可以获取 T 的值
T 可以调用 *T 类型上的方法当 T 类型是可以寻址的时候,这个可寻址参考前面的介绍。编译器会自动的获取 T 的地址传给调用的 receiver,如果不能寻址则不能调用
Go
Copy
type T struct{} func (t T) A() string { return "a" } func (t *T) B() string { return "b" } (&T{}).A() (&T{}).B() (T{}).A() (T{}).B() // cannot call pointer method B on T
指针调用者有副作用 side-effect
Go
Copy
type T struct { Value int } func (t T) Update1() { t.Value = 10 // receiver 和入参一样是值拷贝,此时的修改是在新的 T 上的修改 没有副作用 } func (t *T) Update2() { t.Value = 20 // 虽然也是值拷贝,但是拷贝的是 T 指针,新的 t 还是指向原值。所以对 t 的 Value 的修改直接修改的是原值的 Value 有副作用 } func (t *T) Update3() { t = &T{30} // 拷贝了的 t 指针指向一个新的对象,不会影响原值 } func (t *T) Update4() { *t = T{40} // 对拷贝的 t 指针取值也即原值做修改,对原值发生变化 }

断言

断言不会改变指针的地址,也就是断言后仍然可以改变以前的值
Shell
Copy
var i interface{} i = a c := i.(*A) c. = "hello"

返回本地变量指针是否安全

在函数栈中的变量通过指针返回是否安全呢,是否会出现变量的栈空间被回收但是指针仍然指向这块空间从而导致错误呢?
在 Golang 中是不会的因为编译器会通过逃逸分析,把这个变量分配在堆上所以不会被直接回收所以是安全的
参考:

「真诚赞赏,手留余香」

Foreversmart

真诚赞赏,手留余香

使用微信扫描二维码完成支付