golang利用二级指针操作链表
以下所有观点都是个人愚见,有不同建议或补充的的欢迎emial我aboutme
导读
之前开了个项目来看golang runtime源码https://github.com/zboya/golang_runtime_reading,发现实现全局P链表的代码挺有趣的,遂研究记录一下。
先看个例子
package ptplist
import (
"log"
"testing"
"unsafe"
)
type puintptr uintptr
//go:nosplit
func (pp puintptr) ptr() *p { return (*p)(unsafe.Pointer(pp)) }
//go:nosplit
func (pp *puintptr) set(p *p) { *pp = puintptr(unsafe.Pointer(p)) }
type p struct {
id int
link puintptr
}
var pidleList puintptr
func pidleput(_p_ *p) {
_p_.link = pidleList
pidleList.set(_p_)
}
func pidleget() *p {
_p_ := pidleList.ptr()
if _p_ != nil {
pidleList = _p_.link
}
return _p_
}
func TestPtoPList(t *testing.T) {
var tp *p // 注意tp变量得在for循环外面声明
for i := 0; i < 5; i++ {
tp = &p{id: i}
pidleput(tp)
log.Printf("put p: %d", i)
}
log.Println("")
for i := 0; i < 5; i++ {
tp := pidleget()
log.Printf("get p: %d", tp.id)
}
}
结果:
=== RUN TestPtoPList
2018/08/28 17:25:22 put p: 0
2018/08/28 17:25:22 put p: 1
2018/08/28 17:25:22 put p: 2
2018/08/28 17:25:22 put p: 3
2018/08/28 17:25:22 put p: 4
2018/08/28 17:25:22
2018/08/28 17:25:22 get p: 4
2018/08/28 17:25:22 get p: 3
2018/08/28 17:25:22 get p: 2
2018/08/28 17:25:22 get p: 1
2018/08/28 17:25:22 get p: 0
--- PASS: TestPtoPList (0.00s)
分析
从结果上看pidleList
实现了一个栈,pidleput
把一个新的p push到pidleList
,pidleget
是从pidleList
pop出一个p。
具体看看这个怎么实现的:
type puintptr uintptr
首先我们定义了puintptr类型,底层类型为uintptr。uintptr是整型,可以足够保存指针的值得范围,在32平台下为4字节,在64位平台下是8字节,它和unsafe.Pointer可相互转换。 uintptr和unsafe.Pointer的区别就是:unsafe.Pointer只是单纯的通用指针类型,用于转换不同类型指针,它不可以参与指针运算;而uintptr是可用于指针运算的。
uintptr和unsafe.Pointer互转换
func TestUintprt1(t *testing.T) {
var a int
a = 2018
b := (*int)(unsafe.Pointer(&a))
t.Logf("a.addr=%p, b.addr=%p, *b=%d", &a, b, *b)
buintptr := uintptr((unsafe.Pointer(&a)))
t.Logf("buintptr=0x%x", buintptr)
}
output:
a.addr=0xc42008e1e0, b.addr=0xc42008e1e0, *b=2018
buintptr=0xc42008e1e0
buintptr的值就等于a的地址
uintptr的指针运算取数组
func TestUintprt2(t *testing.T) {
var a [2]int
a[0] = 2018
a[1] = 2019
t.Logf("a0.addr=%p, a1.addr=%p", &a[0], &a[1])
auintptr := uintptr((unsafe.Pointer(&a)))
t.Logf("auintptr=0x%x", auintptr)
// 把auintptr的值加上一个指针的长度, 和C语言的p++一样
a1uintptr := auintptr + unsafe.Sizeof(&a)
a1pointer := unsafe.Pointer(a1uintptr)
// 得到a[1]
a1 := *(*int)(a1pointer)
t.Logf("a1pointer=%p, a1=%d", a1pointer, a1)
}
output:
a0.addr=0xc42009c1e0, a1.addr=0xc42009c1e8
auintptr=0xc42009c1e0
a1pointer=0xc42009c1e8, a1=2019
获取到auintptr表示a数组的首地址,把它加上一个指针长度,就会得到a数组下一个元素。这个和C语言的p++是一样的,只不过golang不推崇指针运算,写起来很麻烦。
//go:nosplit
func (pp puintptr) ptr() *p { return (*p)(unsafe.Pointer(pp)) }
//go:nosplit
func (pp *puintptr) set(p *p) { *pp = puintptr(unsafe.Pointer(p)) }
接着给puintptr类型增加了两个方法,比如现有有个puintptr类型的值pl,那么:
ptr()
方法,将pl变为p类型的指针返回,set(p *p)
方法将p的地址转换为puintptr,并将这puintptr赋值给pl,也就是说pl的值等于p的地址。
这里的pp是pl的指针,*pl就是二级指针了。理解这两个方法就好理解代码了。
func pidleput(_p_ *p) {
// 将pidleList赋值给link ,最开始的时候pidleList为0,但是之后pidleList是上个p的地址
_p_.link = pidleList
// pidleList保存_p_的地址
pidleList.set(_p_)
}
接着定义put方法,_p_.link
保存上个p的地址,pidleList
保存当前_p_
的地址,这里就构成了单向链表,看图最明显
/*
p1 p2
+-------+ +-------+
| id |<-----+ | id |<------ pidleList
+-------+ | +-------+
| link | +----| link |
+-------+ +-------+
*/
所以每当pidleput时将当前p的link指向上个p,pidleList指向最新的p
func pidleget() *p {
// 获取pidleList指向的p
_p_ := pidleList.ptr()
// _p_ 不等于0
if _p_ != nil {
// 将pidleList重新指向下一个p
pidleList = _p_.link
}
return _p_
}
理解了上面的pidleget就简单了。
golang 源码
想具体看源码的话可以看下面的连接:
https://github.com/zboya/golang_runtime_reading/blob/master/src/runtime/proc.go
https://github.com/zboya/golang_runtime_reading/blob/master/src/runtime/runtime2.go