interface
在Go语言的源码位置: src\runtime\runtime2.go中
可以看到对于空的interface,其实就是两个指针。第一个rtype类型, 这个就表示类型基本信息,包括类型的大小,对齐信息,类型的编号
type eface struct { _type *_type //类型指针 data unsafe.Pointer //数据区域指针}
type _type struct { size uintptr
ptrdata uintptr // size of memory prefix holding all pointers hash uint32
tflag tflag
align uint8
fieldalign uint8
kind uint8
alg *typeAlg
// gcdata stores the GC type data for the garbage collector. // If the KindGCProg bit is set in kind, gcdata is a GC program. // Otherwise it is a ptrmask bitmap. See mbitmap.go for details. gcdata *byte str nameOff
ptrToThis typeOff}
对于有方法的interface来说,也是两个指针
第一个itab中存放了类型信息,还有一个fun表示方法表。
type iface struct { tab *itab
data unsafe.Pointer}type itab struct { inter *interfacetype
_type *_type
link *itab
bad int32
inhash int32 // has this itab been added to hash? fun [1]uintptr // variable sized}type interfacetype struct { typ _type
pkgpath name
mhdr []imethod //接口带有的函数名}
带方法的interface举例:
package mainimport ("strconv""fmt")type Stringer interface { String() string}type Binary uint64
func (i Binary) String() string { return strconv.FormatUint(i.Get(), 2)}func (i Binary) Get() uint64 { return uint64(i)}func main() { b := Binary(200) fmt.Println(b.String()) s := Stringer(b) fmt.Println(s.String())}
对于Binary,作为一个64位整数,可以这么表示:
对于s := Stringer(b),可以如下表示:
那么对于s来说
itab中的_type表示的是Stringer这个接口,inter中的typ表示的是Binary这个动态类型,fun函数表中存放的就是Binary中实现了String而接口的方法地址。
对于调用s.String()方法,其实就是 s.itab->fun[0]。
type face struct{ _type unsafe.Pointer data unsafe.Pointer}type fake struct { a string}var test interface{} = nilvar b *fake = nilvar test2 interface{} = b
func main() { if test != nil { fmt.Println("that's not what i want!!!!!! ", *(*face)(unsafe.Pointer(&test)) ) }else{ fmt.Println("wonderful result", *(*face)(unsafe.Pointer(&test))) } if test2 != nil { fmt.Println("that's not what i want!!!!!!", *(*face)(unsafe.Pointer(&test2))) }else{ fmt.Println("wonderful result", *(*face)(unsafe.Pointer(&test2))) }}
采用interface作为返回值类型时,避坑思路:
1、利用error作为返回判断的依据,而不是判断返回的指针
2、不要把为nil的变量赋值给一个interface(推荐)
3、判断interface的data字段,忽略type
切片是对数组中一段数据的引用。
在内存中它有三段数据组成:
指向数据头的指针 ptr
切片的长度 len
切片的容量 cap
长度是索引操作的上界,如:x[i] 。容量是切片操作的上界,如:x[i:j]。
在runtime\slice.go中,我们可以看到, slice的make,copy,grow等函数都在这个文件中实现。
type slice struct { array unsafe.Pointer len int cap int}
Go语言提供了内置的copy和append函数来增长切片的容量。
copy方法并不会修改slice的内存模型,仅仅是将某个slice的内容拷贝到另外一个slice中去。
func (tGen *tInfo) Copy []t { if tGen.ts == nil { return nil } newField := make([]t, len(tGen.ts)) fmt.Println(*(*sliceA)(unsafe.Pointer(&newField))) copy(newField, tGen.ts) fmt.Println(*(*sliceA)(unsafe.Pointer(&newField))) return newField}
打印结果:
append方法其实重新生成了一个新的数组,然后返回的切片引用了这个新的数组
fmt.Println(*(*sliceA)(unsafe.Pointer(&results))) a := t{"aaa", "ddd"} results = append(results, a) fmt.Println(*(*sliceA)(unsafe.Pointer(&results)))
打印结果:
append具体实现的代码看不到,但过程其实就是判断cap,生成一个新的数组,将old的元素拷贝到新的slice中去。
扩容规则:
如果新的大小是当前大小2倍以上,则大小增长为新大小
否则循环以下操作:如果当前大小小于1024,按每次2倍增长,否则每次按当前大小1/4增长。直到增长的大小超过或等于新大小。
1、下面的修改,把切片的引用改掉了,导致newField引用被修改,出错
// KoratCopy_taiRanges_Fastfunc (tGen *tInfo) Copy(newSlice0 []t) { for index1, oldStruct1 := range tGen.ts { newSlice0[index1] = oldStruct1
oldStruct1.CopyFast(&newSlice0[index1]) }}// KoratCopyFastfunc (tGen *t) CopyFast(newStruct *t) { *newStruct = *tGen}func (tGen *tInfo) KCopy []t { if tGen.ts == nil { return nil } newField := make([]t, len(tGen.t)) tGen.Copy(newField) return newField}
func (tGen *tfo) KCopy []t { if tGen.taiRanges == nil { return nil } newField := make([]t, len(tGen.ts)) newField = tGen.ts
return newField}
上面我们可以看到pool创建的时候是不能指定大小的,所有sync.Pool的缓存对象数量是没有限制的(只受限于内存),因此使用sync.pool是没办法做到控制缓存对象数量的个数的。另外sync.pool缓存对象的期限是很诡异的,先看一下src/pkg/sync/pool.go里面的一段实现代码:
func init() {
runtime_registerPoolCleanup(poolCleanup) }
可以看到pool包在init的时候注册了一个poolCleanup函数,它会清除所有的pool里面的所有缓存的对象,该函数注册进去之后会在每次gc之前都会调用,因此sync.Pool缓存的期限只是两次gc之间这段时间。
如何在多个goroutine之间使用同一个pool做到高效呢?官方的做法就是尽量减少竞争,因为sync.pool为每个P(对应cpu,不了解的童鞋可以去看看golang的调度模型介绍)都分配了一个子池,如下图:
当执行一个pool的get或者put操作的时候都会先把当前的goroutine固定到某个P的子池上面,然后再对该子池进行操作。每个子池里面有一个私有对象和共享列表对象,私有对象是只有对应的P能够访问,因为一个P同一时间只能执行一个goroutine,因此对私有对象存取操作是不需要加锁的。共享列表是和其他P分享的,因此操作共享列表是需要加锁的。
获取对象过程是:
1)固定到某个P,尝试从私有对象获取,如果私有对象非空则返回该对象,并把私有对象置空;
2)如果私有对象是空的时候,就去当前子池的共享列表获取(需要加锁);
3)如果当前子池的共享列表也是空的,那么就尝试去其他P的子池的共享列表偷取一个(需要加锁);
4)如果其他子池都是空的,最后就用用户指定的New函数产生一个新的对象返回。
可以看到一次get操作最少0次加锁,最大N(N等于MAXPROCS)次加锁。
归还对象的过程:
1)固定到某个P,如果私有对象为空则放到私有对象;
2)否则加入到该P子池的共享列表中(需要加锁)。
可以看到一次put操作最少0次加锁,最多1次加锁。
由于goroutine具体会分配到那个P执行是golang的协程调度系统决定的,因此在MAXPROCS>1的情况下,多goroutine用同一个sync.Pool的话,各个P的子池之间缓存的对象是否平衡以及开销如何是没办法准确衡量的。但如果goroutine数目和缓存的对象数目远远大于MAXPROCS的话,概率上说应该是相对平衡的。
总的来说,sync.Pool的定位不是做类似连接池的东西,它的用途仅仅是增加对象重用的几率,减少gc的负担,而开销方面也不是很便宜的。
“nil”标志符用于表示interface、函数、maps、slices和channels的“零值”。如果你不指定变量的类型,编译器将无法编译你的代码,因为它猜不出具体的类型。