皮皮网

【易得网 源码】【libvirt源码安装】【dm游戏源码】golang reflect 源码

2024-12-25 15:23:48 来源:npm 模块源码

1.关于反射
2.Golang源码剖析panic与recover,看不懂你打我好了
3.Go看源码必会知识之unsafe包
4.深入理解 golang 中的反射机制
5.golangpanic的实现原理?
6.Golang面试知识点总结

golang reflect 源码

关于反射

       åœ¨è®¡ç®—机科学领域,反射是指一类应用,它们能够自描述和自控制。也就是说,这类应用通过采用某种机制来实现对自己行为的描述(self-representation)和监测(examination),并能根据自身行为的状态和结果,调整或修改应用所描述行为的状态和相关的语义。

        每种语言的反射模型都不同,并且有些语言根本不支持反射。Golang语言实现了反射,反射机制就是在运行时动态的调用对象的方法和属性,官方自带的reflect包就是反射相关的,只要包含这个包就可以使用。

        多插一句,Golang的gRPC也是通过反射实现的。

        在讲反射之前,先来看看Golang关于类型设计的一些原则

        接下来要讲的反射,就是建立在类型之上的,Golang的指定类型的变量的类型是静态的(也就是指定int、string这些的变量,它的type是static type),在创建变量的时候就已经确定,反射主要与Golang的interface类型相关(它的type是concrete type),只有interface类型才有反射一说。

        在Golang的实现中,每个interface变量都有一个对应pair,pair中记录了实际变量的值和类型:

        value是实际变量值,type是实际变量的类型。一个interface{ }类型的变量包含了2个指针,一个指针指向值的类型【对应concrete type】,另外一个指针指向实际的值【对应value】。

        例如,创建类型为*os.File的变量,然后将其赋给一个接口变量r:

        接口变量r的pair中将记录如下信息:(tty, *os.File),这个pair在接口变量的连续赋值过程中是不变的,将接口变量r赋给另一个接口变量w:

        接口变量w的pair与r的pair相同,都是:(tty, *os.File),即使w是空接口类型,pair也是不变的。

        interface及其pair的存在,是Golang中实现反射的前提,理解了pair,就更容易理解反射。反射就是用来检测存储在接口变量内部(值value;类型concrete type) pair对的一种机制。

        既然反射就是用来检测存储在接口变量内部(值value;类型concrete type) pair对的一种机制。那么在Golang的reflect反射包中有什么样的方式可以让我们直接获取到变量内部的信息呢? 它提供了两种类型(或者说两个方法)让我们可以很容易的访问接口变量内容,分别是reflect.ValueOf() 和 reflect.TypeOf(),看看官方的解释

        reflect.TypeOf()是获取pair中的type,reflect.ValueOf()获取pair中的value,示例如下:

        当执行reflect.ValueOf(interface)之后,就得到了一个类型为”relfect.Value”变量,可以通过它本身的Interface()方法获得接口变量的真实内容,然后可以通过类型判断进行转换,转换为原有真实类型。不过,我们可能是已知原有类型,也有可能是未知原有类型,因此,下面分两种情况进行说明。

        已知类型后转换为其对应的类型的做法如下,直接通过Interface方法然后强制转换,如下:

        示例如下:

        很多情况下,我们可能并不知道其具体类型,那么这个时候,该如何做呢?需要我们进行遍历探测其Filed来得知,示例如下:

        通过运行结果可以得知获取未知类型的interface的具体变量及其类型的步骤为:

        通过运行结果可以得知获取未知类型的interface的所属方法(函数)的步骤为:

        reflect.Value是通过reflect.ValueOf(X)获得的,只有当X是指针的时候,才可以通过reflec.Value修改实际变量X的值,即:要修改反射类型的对象就一定要保证其值是“addressable”的。

        示例如下:

        这算是一个高级用法了,前面我们只说到对类型、变量的几种反射的用法,包括如何获取其值、其类型、如果重新设置新值。但是在工程应用中,另外一个常用并且属于高级的用法,就是通过reflect来进行方法【函数】的调用。比如我们要做框架工程的时候,需要可以随意扩展方法,或者说用户可以自定义方法,那么我们通过什么手段来扩展让用户能够自定义呢?关键点在于用户的自定义方法是未可知的,因此我们可以通过reflect来搞定

        示例如下:

        Golang的反射很慢,这个和它的API设计有关。在 java 里面,我们一般使用反射都是这样来弄的。

        这个取得的反射对象类型是 java.lang.reflect.Field。它是可以复用的。只要传入不同的obj,就可以取得这个obj上对应的 field。

        但是Golang的反射不是这样设计的:

        这里取出来的 field 对象是 reflect.StructField 类型,但是它没有办法用来取得对应对象上的值。如果要取值,得用另外一套对object,而不是type的反射

        这里取出来的 fieldValue 类型是 reflect.Value,它是一个具体的值,而不是一个可复用的反射对象了,每次反射都需要malloc这个reflect.Value结构体,并且还涉及到GC。

        Golang reflect慢主要有两个原因

        上述详细说明了Golang的反射reflect的各种功能和用法,都附带有相应的示例,相信能够在工程应用中进行相应实践,总结一下就是:

Golang源码剖析panic与recover,看不懂你打我好了

       哈喽,大家好,我是asong,今天与大家来聊一聊go语言中的易得网 源码"throw、try.....catch{ }"。如果你之前是一名java程序员,我相信你一定吐槽过go语言错误处理方式,但是这篇文章不是来讨论好坏的,我们本文的重点是带着大家看一看panic与recover是如何实现的。上一文我们讲解了defer是如何实现的,但是没有讲解与defer紧密相连的recover,想搞懂panic与recover的实现也没那么简单,就放到这一篇来讲解了。废话不多说,直接开整。

       Go 语言中panic 关键字主要用于主动抛出异常,类似 java 等语言中的 throw 关键字。panic 能够改变程序的控制流,调用 panic 后会立刻停止执行当前函数的剩余代码,并在当前 Goroutine 中递归执行调用方的 defer;

       Go 语言中recover 关键字主要用于捕获异常,让程序回到正常状态,类似 java 等语言中的 try ... catch 。recover 可以中止 panic 造成的程序崩溃。它是一个只能在 defer 中发挥作用的函数,在其他作用域中调用不会发挥作用;

       recover只能在defer中使用这个在标准库的注释中已经写明白了,我们可以看一下:

       这里有一个要注意的点就是recover必须要要在defer函数中使用,否则无法阻止panic。最好的验证方法是先写两个例子:

       运行我们会发现example2()方法的panic是没有被recover住的,导致整个程序直接crash了。这里大家肯定会有疑问,为什么直接写recover()就不能阻止panic了呢。我们在 详解defer实现机制(附上三道面试题,我不信你们都能做对)讲解了defer实现原理,一个重要的知识点**defer将语句放入到栈中时,也会将相关的值拷贝同时入栈。**所以defer recover()这种写法在放入defer栈中时就已经被执行过了,panic是发生在之后,所以根本无法阻止住panic。

       通过运行结果可以看出panic不会影响defer函数的使用,所以他是安全的。

       这里我开了两个协程,一个协程会发生panic,导致程序崩溃,但是只会执行自己所在Goroutine的延迟函数,所以正好验证了多个 Goroutine 之间没有太多的关联,一个 Goroutine 在 panic 时也不应该执行其他 Goroutine 的延迟函数。

       其实我们在实际项目开发中,经常会遇到panic问题, Go 的 runtime 代码中很多地方都调用了 panic 函数,对于不了解 Go 底层实现的新人来说,这无疑是挖了一堆深坑。我们在实际生产环境中总会出现panic,但是我们的程序仍能正常运行,这是因为我们的框架已经做了recover,他已经为我们兜住底,比如gin,我们看一看他是怎么做的。

       我们先来写个简单的代码,看看他的汇编调用:执行go tool compile -N -l -S main.go就可以看到对应的汇编码了,我们截取部分片段分析:

       上面重点部分就是画红线的三处,第一步调用runtime.deferprocStack创建defer对象,这一步大家可能会有疑惑,我上一文忘记讲个这个了,这里先简单概括一下,defer总共有三种模型,编译一个函数里只会有一种defer模式。在讲defer实现机制时,我们一起看过defer的结构,其中有一个字段就是_panic,是libvirt源码安装触发defer的作用,我们来看看的panic的结构:

       简单介绍一下上面的字段:

       上面的pc、sp、goexit我们单独讲一下,runtime包中有一个Goexit方法,Goext能够终止调用它的goroutine,其他的goroutine是不受影响的,goexit也会在终止goroutine之前运行所有延迟调用函数,Goexit不是一个panic,所以这些延迟函数中的任何recover调用都将返回nil。如果我们在主函数中调用了Goexit会终止该goroutine但不会返回func main。由于func main没有返回,因此程序将继续执行其他gorountine,直到所有其他goroutine退出,程序才会crash。

       下面就开始我们的重点吧~。

       在讲defer实现机制时,我们一起看过defer的结构,其中有一个字段就是_panic,是触发defer的作用,我们来看看的panic的结构:简单介绍一下上面的字段:上面的pc、sp、goexit我们单独讲一下,runtime包中有一个Goexit方法,Goext能够终止调用它的goroutine,其他的goroutine是不受影响的,goexit也会在终止goroutine之前运行所有延迟调用函数,Goexit不是一个panic,所以这些延迟函数中的任何recover调用都将返回nil。如果我们在主函数中调用了Goexit会终止该goroutine但不会返回func main。由于func main没有返回,因此程序将继续执行其他gorountine,直到所有其他goroutine退出,程序才会crash。写个简单的例子:运行上面的例子你就会发现,即使在主goroutine中调用了runtime.Goexit,其他goroutine是没有任何影响的。所以结构中的pc、sp、goexit三个字段都是为了修复runtime.Goexit,这三个字段就是为了保证该函数的一定会生效,因为如果在defer中发生panic,那么goexit函数就会被取消,所以才有了这三个字段做保护。看这个例子:

       英语好的可以看一看这个: github.com/golang/go/is...,这就是上面的一个例子,这里就不过多解释了,了解就好。

       接下来我们再来看一看gopanic方法。

       gopanic的代码有点长,我们一点一点来分析:

       根据不同的类型判断当前发生panic错误,这里没什么多说的,接着往下看。

       上面的代码都是截段,这些部分都是为了判断当前defer是否可以使用开发编码模式,具体怎么操作的就不展开了。

       在第三部分进行defer内联优化选择时会执行调用延迟函数(reflectcall就是这个作用),也就是会调用runtime.gorecover把recoverd = true,具体这个函数的操作留在下面讲,因为runtime.gorecover函数并不包含恢复程序的逻辑,程序的恢复是在gopanic中执行的。先看一下代码:

       这段代码有点长,主要就是分为两部分:

       第一部分主要是这个判断if gp._panic != nil && gp._panic.goexit && gp._panic.aborted { ... },正常recover是会绕过Goexit的,所以为了解决这个,添加了这个判断,这样就可以保证Goexit也会被recover住,这里是通过从runtime._panic中取出了程序计数器pc和栈指针sp并且调用runtime.recovery函数触发goroutine的调度,调度之前会准备好 sp、pc 以及函数的返回值。

       第二部分主要是做panic的recover,这也与上面的dm游戏源码流程基本差不多,他是从runtime._defer中取出了程序计数器pc和栈指针sp并调用recovery函数触发Goroutine,跳转到recovery函数是通过runtime.call进行的,我们看一下其源码(src/runtime/asm_amd.s 行):

       因为go语言中的runtime环境是有自己的堆栈和goroutine,recovery函数也是在runtime环境执行的,所以要调度到m->g0来执行recovery函数,我们在看一下recovery函数:

       在recovery 函数中,利用 g 中的两个状态码回溯栈指针 sp 并恢复程序计数器 pc 到调度器中,并调用 gogo 重新调度 g , goroutine 继续执行,recovery在调度过程中会将函数的返回值设置为1。这个有什么作用呢? 在deferproc函数中找到了答案:

       当延迟函数中recover了一个panic时,就会返回1,当 runtime.deferproc 函数的返回值是 1 时,编译器生成的代码会直接跳转到调用方函数返回之前并执行 runtime.deferreturn,跳转到runtime.deferturn函数之后,程序就已经从panic恢复了正常的逻辑。

       在这里runtime.fatalpanic实现了无法被恢复的程序崩溃,它在中止程序之前会通过 runtime.printpanics 打印出全部的 panic 消息以及调用时传入的参数。

       这就是这个逻辑流程,累死我了。。。。

       结尾给大家发一个小福利,哈哈,这个福利就是如果避免出现panic,要注意这些:这几个是比较典型的,还有很多会发生panic的地方,交给你们自行学习吧~。

       好啦,这篇文章就到这里啦,素质三连(分享、点赞、在看)都是笔者持续创作更多优质内容的动力!

Go看源码必会知识之unsafe包

       前言

       有看源码的朋友应该会发现,Go标准库中大量使用了unsafe.pointer,要想更好的理解源码实现,就要知道unsafe.pointer到底是什么?所以今天就与大家来聊一聊unsafe包。

什么是unsafe

       众所周知,Go语言被设计成一门强类型的静态语言,那么他的类型就不能改变了,静态也是意味着类型检查在运行前就做了。所以在Go语言中是不允许两个指针类型进行转换的,使用过C语言的朋友应该知道这在C语言中是可以实现的,Go中不允许这么使用是处于安全考虑,毕竟强制转型会引起各种各样的麻烦,有时这些麻烦很容易被察觉,有时他们却又隐藏极深,难以察觉。大多数读者可能不明白为什么类型转换是不安全的,这里用C语言举一个简单的例子:

int main(){ double pi = 3.;double *pv = πvoid *temp = pd;int *p = temp;}

       在标准C语言中,任何非void类型的指针都可以和void类型的指针相互指派,也可以通过void类型指针作为中介,实现不同类型的指针间接相互转换。上面示例中,指针pv指向的空间本是一个双精度数据,占8个字节,但是经过转换后,p指向的是一个4字节的int类型。这种发生内存截断的设计缺陷会在转换后进行内存访问是存在安全隐患。我想这就是Go语言被设计成强类型语言的原因之一吧。

       虽然类型转换是不安全的,但是在一些特殊场景下,使用了它,可以打破Go的类型和内存安全机制,可以绕过类型系统低效,提高运行效率。所以Go标准库中提供了一个unsafe包,之所以叫这个名字,就是不推荐大家使用,但是nodejs 源码解读不是不能用,如果你掌握的特别好,还是可以实践的。

unsafe 实现原理

       在使用之前我们先来看一下unsafe的源码部分,标准库unsafe包中只提供了3``种方法,分别是:

func Sizeof(x ArbitraryType) uintptrfunc Offsetof(x ArbitraryType) uintptrfunc Alignof(x ArbitraryType) uintptr

       Sizeof(x ArbitrayType)方法主要作用是用返回类型x所占据的字节数,但并不包含x所指向的内容的大小,与C语言标准库中的Sizeof()方法功能一样,比如在位机器上,一个指针返回大小就是4字节。

       Offsetof(x ArbitraryType)方法主要作用是返回结构体成员在内存中的位置离结构体起始处(结构体的第一个字段的偏移量都是0)的字节数,即偏移量,我们在注释中看一看到其入参必须是一个结构体,其返回值是一个常量。

       Alignof(x ArbitratyType)的主要作用是返回一个类型的对齐值,也可以叫做对齐系数或者对齐倍数。对齐值是一个和内存对齐有关的值,合理的内存对齐可以提高内存读写的性能。一般对齐值是2^n,最大不会超过8(受内存对齐影响).获取对齐值还可以使用反射包的函数,也就是说:unsafe.Alignof(x)等价于reflect.TypeOf(x).Align()。对于任意类型的变量x,unsafe.Alignof(x)至少为1。对于struct结构体类型的变量x,计算x每一个字段f的unsafe.Alignof(x,f),unsafe.Alignof(x)等于其中的最大值。对于array数组类型的变量x,unsafe.Alignof(x)等于构成数组的元素类型的对齐倍数。没有任何字段的空struct{ }和没有任何元素的array占据的内存空间大小为0,不同大小为0的变量可能指向同一块地址。

       细心的朋友会发发现这三个方法返回的都是uintptr类型,这个目的就是可以和unsafe.poniter类型相互转换,因为*T是不能计算偏移量的,也不能进行计算,但是uintptr是可以的,所以可以使用uintptr类型进行计算,这样就可以可以访问特定的内存了,达到对不同的内存读写的目的。三个方法的入参都是ArbitraryType类型,代表着任意类型的意思,同时还提供了一个Pointer指针类型,即像void *一样的通用型指针。

type ArbitraryType inttype Pointer *ArbitraryType// uintptr 是一个整数类型,它足够大,可以存储type uintptr uintptr

       上面说了这么多,可能会有点懵,在这里对三种指针类型做一个总结:

       *T:普通类型指针类型,用于传递对象地址,不能进行指针运算。

       unsafe.poniter:通用指针类型,用于转换不同类型的指针,不能进行指针运算,不能读取内存存储的值(需转换到某一类型的普通指针)

       uintptr:用于指针运算,GC不把uintptr当指针,uintptr无法持有对象。uintptr类型的目标会被回收。

       三者关系就是:unsafe.Pointer是桥梁,可以让任意类型的指针实现相互转换,也可以将任意类型的指针转换为uintptr进行指针运算,也就说uintptr是用来与unsafe.Pointer打配合,用于指针运算。画个图表示一下:

       基本原理就说到这里啦,接下来我们一起来看看如何使用~

unsafe.Pointer基本使用

       我们在上一篇分析atomic.Value源码时,看到atomic/value.go中定义了一个ifaceWords结构,其中typ和data字段类型就是unsafe.Poniter,这里使用unsafe.Poniter类型的原因是传入的值就是interface{ }类型,使用unsafe.Pointer强转成ifaceWords类型,这样可以把类型和值都保存了下来,方便后面的写入类型检查。截取部分代码如下:

// ifaceWords is interface{ } internal representation.type ifaceWords struct { typunsafe.Pointer data unsafe.Pointer}// Load returns the value set by the most recent Store.// It returns nil if there has been no call to Store for this Value.func (v *Value) Load() (x interface{ }) { vp := (*ifaceWords)(unsafe.Pointer(v))for { typ := LoadPointer(&vp.typ) // 读取已经存在值的类型/**..... 中间省略**/// First store completed. Check type and overwrite data.if typ != xp.typ { //当前类型与要存入的类型做对比 panic("sync/atomic: store of inconsistently typed value into Value")}}

       上面就是源码中使用unsafe.Pointer的一个例子,有一天当你准备读源码时,微政务源码unsafe.pointer的使用到处可见。好啦,接下来我们写一个简单的例子,看看unsafe.Pointer是如何使用的。

func main(){ number := 5 pointer := &number fmt.Printf("number:addr:%p, value:%d\n",pointer,*pointer) floatNumber := (*float)(unsafe.Pointer(pointer)) *floatNumber = *floatNumber + 3 fmt.Printf("float:addr:%p, value:%f\n",floatNumber,*floatNumber)}

       运行结果:

number:addr:0xc, value:5float:addr:0xc, value:3.

       由运行可知使用unsafe.Pointer强制类型转换后指针指向的地址是没有改变,只是类型发生了改变。这个例子本身没什么意义,正常项目中也不会这样使用。

       总结一下基本使用:先把*T类型转换成unsafe.Pointer类型,然后在进行强制转换转成你需要的指针类型即可。

Sizeof、Alignof、Offsetof三个函数的基本使用

       先看一个例子:

type User struct { Name string Age uint Gender bool // 男:true 女:false 就是举个例子别吐槽我这么用。。。。}func func_example(){ // sizeof fmt.Println(unsafe.Sizeof(true)) fmt.Println(unsafe.Sizeof(int8(0))) fmt.Println(unsafe.Sizeof(int())) fmt.Println(unsafe.Sizeof(int())) fmt.Println(unsafe.Sizeof(int())) fmt.Println(unsafe.Sizeof("asong")) fmt.Println(unsafe.Sizeof([]int{ 1,3,4})) // Offsetof user := User{ Name: "Asong", Age: ,Gender: true} userNamePointer := unsafe.Pointer(&user) nNamePointer := (*string)(unsafe.Pointer(userNamePointer)) *nNamePointer = "Golang梦工厂" nAgePointer := (*uint)(unsafe.Pointer(uintptr(userNamePointer) + unsafe.Offsetof(user.Age))) *nAgePointer = nGender := (*bool)(unsafe.Pointer(uintptr(userNamePointer)+unsafe.Offsetof(user.Gender))) *nGender = false fmt.Printf("u.Name: %s, u.Age: %d,u.Gender: %v\n", user.Name, user.Age,user.Gender) // Alignof var b bool var i8 int8 var i int var i int var f float var s string var m map[string]string var p *int fmt.Println(unsafe.Alignof(b)) fmt.Println(unsafe.Alignof(i8)) fmt.Println(unsafe.Alignof(i)) fmt.Println(unsafe.Alignof(i)) fmt.Println(unsafe.Alignof(f)) fmt.Println(unsafe.Alignof(s)) fmt.Println(unsafe.Alignof(m)) fmt.Println(unsafe.Alignof(p))}

       为了省事,把三个函数的使用示例放到了一起,首先看sizeof方法,我们可以知道各个类型所占字节大小,这里重点说一下int类型,Go语言中的int类型的具体大小是跟机器的 CPU位数相关的。如果 CPU 是 位的,那么int就占4字节,如果 CPU是位的,那么 int 就占8 字节,这里我的电脑是位的,所以结果就是8字节。

       然后我们在看Offsetof函数,我想要修改结构体中成员变量,第一个成员变量是不需要进行偏移量计算的,直接取出指针后转换为unsafe.pointer,在强制给他转换成字符串类型的指针值即可。如果要修改其他成员变量,需要进行偏移量计算,才可以对其内存地址修改,所以Offsetof方法就可返回成员变量在结构体中的偏移量,也就是返回结构体初始位置到成员变量之间的字节数。看代码时大家应该要住uintptr的使用,不可以用一个临时变量存储uintptr类型,前面我们提到过用于指针运算,GC不把uintptr当指针,uintptr无法持有对象。uintptr类型的目标会被回收,所以你不知道他什么时候会被GC掉,那样接下来的内存操作会发生什么样的错误,咱也不知道。比如这样一个例子:

// 切记不要这样使用p1 := uintptr(userNamePointer)nAgePointer := (*uint)(unsafe.Pointer(p1 + unsafe.Offsetof(user.Age)))

       最后看一下Alignof函数,主要是获取变量的对齐值,除了int、uintptr这些依赖CPU位数的类型,基本类型的对齐值都是固定的,结构体中对齐值取他的成员对齐值的最大值,结构体的对齐涉及到内存对齐,我们在下面详细介绍。

经典应用:string与[]byte的相互转换

       实现string与byte的转换,正常情况下,我们可能会写出这样的标准转换:

// string to []bytestr1 := "Golang梦工厂"by := []byte(s1)// []byte to stringstr2 := string(by)

       使用这种方式进行转换都会涉及底层数值的拷贝,所以想要实现零拷贝,我们可以使用unsafe.Pointer来实现,通过强转换直接完成指针的指向,从而使string和[]byte指向同一个底层数据。在reflect包中有·string和slice对应的结构体,他们的分别是:

type StringHeader struct { Data uintptr Lenint}type SliceHeader struct { Data uintptr Lenint Capint}

       StringHeader代表的是string运行时的表现形式(SliceHeader同理),通过对比string和slice运行时的表达可以看出,他们只有一个Cap字段不同,所以他们的内存布局是对齐的,所以可以通过unsafe.Pointer进行转换,因为可以写出如下代码:

func Sizeof(x ArbitraryType) uintptrfunc Offsetof(x ArbitraryType) uintptrfunc Alignof(x ArbitraryType) uintptr0

       上面的代码我们通过重新构造slice header和string header完成了类型转换,其实[]byte转换成string可以省略掉自己构造StringHeader的方式,直接使用强转就可以,因为string的底层也是[]byte,强转会自动构造,省略后的代码如下:

func Sizeof(x ArbitraryType) uintptrfunc Offsetof(x ArbitraryType) uintptrfunc Alignof(x ArbitraryType) uintptr1

       虽然这种方式更高效率,但是不推荐大家使用,前面也提高到了,这要是不安全的,使用当不当会出现极大的隐患,一些严重的情况recover也不能捕获。

内存对齐

       现在计算机中内存空间都是按照byte划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但是实际情况是在访问特定类型变量的时候经常在特定的内存地址访问,这就需要各种类型数据按照一定的规则在空间上排列,而不是顺序的一个接一个的排放,这就对齐。

       对齐的作用和原因:CPU访问内存时,并不是逐个字节访问,而是以字长(word size)单位访问。比如位的CPU,字长为4字节,那么CPU访问内存的单位也是4字节。这样设计可以减少CPU访问内存的次数,加大CPU访问内存的吞吐量。假设我们需要读取8个字节的数据,一次读取4个字节那么就只需读取2次就可以。内存对齐对实现变量的原子性操作也是有好处的,每次内存访问都是原子的,如果变量的大小不超过字长,那么内存对齐后,对该变量的访问就是原子的,这个特性在并发场景下至关重要。

       我们来看这样一个例子:

func Sizeof(x ArbitraryType) uintptrfunc Offsetof(x ArbitraryType) uintptrfunc Alignof(x ArbitraryType) uintptr2

       从结果可以看出,字段放置不同的顺序,占用内存也不一样,这就是因为内存对齐影响了struct的大小,所以有时候合理的字段可以减少内存的开销。下面我们就一起来分析一下内存对齐,首先要明白什么是内存对齐的规则,C语言的对齐规则与Go语言一样,所以C语言的对齐规则对Go同样适用:

       对于结构的各个成员,第一个成员位于偏移为0的位置,结构体第一个成员的偏移量(offset)为0,以后每个成员相对于结构体首地址的 offset 都是该成员大小与有效对齐值中较小那个的整数倍,如有需要编译器会在成员之间加上填充字节。

       除了结构成员需要对齐,结构本身也需要对齐,结构的长度必须是编译器默认的对齐长度和成员中最长类型中最小的数据大小的倍数对齐。

       好啦,知道规则了,我们现在来分析一下上面的例子,根据我的mac使用的位CPU,对齐参数是8来分析,int、[]int、string、bool对齐值分别是4、8、8、1,占用内存大小分别是4、、、1,我们先根据第一条对齐规则分析User1:

       第一个字段类型是int,对齐值是4,大小为4,所以放在内存布局中的第一位.

       第二个字段类型是[]int,对齐值是8,大小为,所以他的内存偏移值必须是8的倍数,所以在当前user1中,就不能从第4位开始了,必须从第5位开始,也就偏移量为8。第4,5,6,7位由编译器进行填充,一般为0值,也称之为空洞。第9位到第位为第二个字段B.

       第三个字段类型是string,对齐值是8,大小为,所以他的内存偏移值必须是8的倍数,因为user1前两个字段就已经排到了第位,所以下一位的偏移量正好是,正好是字段C的对齐值的倍数,不用填充,可以直接排列第三个字段,也就是从第位到位第三个字段C.

       第三个字段类型是bool,对齐值是1,大小为1,所以他的内存偏移值必须是1的倍数,因为user1前两个字段就已经排到了第位,所以下一位的偏移量正好是。正好是字段D的对齐值的倍数,不用填充,可以直接排列到第四个字段,也就是从到第位是第三个字段D.

       好了现在第一条内存对齐规则后,内存长度已经为字节,我们开始使用内存的第2条规则进行对齐。根据第二条规则,默认对齐值是8,字段中最大类型程度是,取最小的那一个,所以求出结构体的对齐值是8,我们目前的内存长度是,不是8的倍数,所以需要补齐,所以最终的结果就是,补了7位。

       说了这么多,画个图看一下吧:

       现在你们应该懂了吧,按照这个思路再去分析其他两个struct吧,这里就不再分析了。

       对于内存对齐这里还有一最后需要注意的知识点,空struct不占用任何存储空间,空 struct{ } 大小为 0,作为其他 struct 的字段时,一般不需要内存对齐。但是有一种情况除外:即当 struct{ } 作为结构体最后一个字段时,需要内存对齐。因为如果有指针指向该字段, 返回的地址将在结构体之外,如果此指针一直存活不释放对应的内存,就会有内存泄露的问题(该内存不因结构体释放而释放)。来看一个例子:

func Sizeof(x ArbitraryType) uintptrfunc Offsetof(x ArbitraryType) uintptrfunc Alignof(x ArbitraryType) uintptr3

       简单来说,对于任何占用0大小空间的类型,像struct { }或者[0]byte这些,如果该类型出现在结构体末尾,那么我们就假设它占用1个字节的大小。因此对于test1结构体,他看起来就是这样:`

func Sizeof(x ArbitraryType) uintptrfunc Offsetof(x ArbitraryType) uintptrfunc Alignof(x ArbitraryType) uintptr4

       因此在内存对齐时,最后结构体占用的字节就是8了。

       重点要注意的问题:不要在结构体定义的最后添加零大小的类型

总结

       好啦,终于又到文章的末尾了,我们来简单的总结一下,unsafe 包绕过了 Go 的类型系统,达到直接操作内存的目的,使用它有一定的风险性。但是在某些场景下,使用 unsafe 包提供的函数会提升代码的效率,Go 源码中也是大量使用 unsafe 包。

       unsafe 包定义了 Pointer 和三个函数:

type ArbitraryType inttype Pointer *ArbitraryTypefunc Sizeof(x ArbitraryType) uintptrfunc Offsetof(x ArbitraryType) uintptrfunc Alignof(x ArbitraryType) uintptr

       uintptr 可以和 unsafe.Pointer 进行相互转换,uintptr 可以进行数学运算。这样,通过 uintptr 和 unsafe.Pointer 的结合就解决了 Go 指针不能进行数学运算的限制。通过 unsafe 相关函数,可以获取结构体私有成员的地址,进而对其做进一步的读写操作,突破 Go 的类型安全限制。

       最后我们又学习了内存对齐的知识,这样设计可以减少CPU访问内存的次数,加大CPU访问内存的吞吐量,所以结构体中字段合理的排序可以更节省内存,注意:不要在结构体定义的最后添加零大小的类型。

原文:/post/

       好啦,这篇文章就到这里啦,素质三连(分享、点赞、在看)都是笔者持续创作更多优质内容的动力!

       创建了一个Golang学习交流群,欢迎各位大佬们踊跃入群,我们一起学习交流。入群方式:加我vx拉你入群,或者公众号获取入群二维码

       结尾给大家发一个小福利吧,最近我在看[微服务架构设计模式]这一本书,讲的很好,自己也收集了一本PDF,有需要的小伙可以到自行下载。获取方式:关注公众号:[Golang梦工厂],后台回复:[微服务],即可获取。

       我翻译了一份GIN中文文档,会定期进行维护,有需要的小伙伴后台回复[gin

深入理解 golang 中的反射机制

       深入理解 Golang 中的反射机制

       反射是计算机科学中的一个重要概念,程序通过反射可以在运行时访问、检测和修改自身的状态和行为。在 Golang 这种静态类型的编译型语言中,反射机制通过内置的 reflect 包实现,包含 reflect.Type 和 reflect.Value 两个主要类型。

       在 Golang 中,使用反射可以灵活地获取变量的类型和值。例如,通过 reflect.TypeOf() 函数,可以获取变量的类型,如:

       通过 reflect.ValueOf() 函数,可以获取变量的值,如:

       可以使用 reflect.Value 的 Set() 方法修改变量的值,如:

       反射机制在 Golang 中有多种应用场景,如动态类型检查、元编程、插件系统等。

       然而,反射的使用需谨慎考虑其性能影响。反射操作通常比直接操作慢,主要体现在类型检查、方法调用和属性访问等方面。因此,在实际应用中,应权衡反射的灵活性与性能成本。

       总之,反射机制是 Golang 中的强大特性,能够提供程序运行时的灵活性和扩展性。了解和掌握反射的使用方法、应用场景以及性能考量,有助于编写更高效、可维护的代码。

golangpanic的实现原理?

       要了解panic机制,首先需对defer原理有所掌握,本文仅专注于panic内容。

       panic是一个包含defer指针、参数、panic列表表头指针和已恢复或终止信息的结构体。此结构体在后续版本中会进一步扩展,以优化panic和recover性能。

       在runtime.gopanic方法中,处理流程如下:每个goroutine都拥有一个panic链表。遇panic代码生成对应_panic数据,存入链表表头。每执行完一个函数,如无panic,跳过_panic数据,继续正常流程;若遇panic,处理链表中对应_panic。

       如果函数内存在defer,按约定顺序执行延迟代码。执行完毕后,若需recover,由reflectcall调用gorecover。执行recover后,recovered字段标记为真,由recovery方法负责处理,恢复至正常流程。若无recover,进入死给你看流程。

       比较厚道的是,在打印出涉及的panic消息后,执行fatalpanic方法,宣告程序结束。

       当执行defer链表中的defer时,可能产生新panic,此时当前_panic被标记为放弃,进入新产生的_panic处理流程。

       需注意,Golang的goroutine机制下,panic在不同goroutine中是独立处理的。一个地方出问题可能导致整个程序结束,使用时应格外小心。

Golang面试知识点总结

       本文总结了Go语言开发所需掌握的关键知识点,涵盖了语言特性、并发机制、内存管理、数据结构、反射原理等核心内容,旨在帮助开发者构建全面的Go语言知识体系。

       ### 数组与切片

       切片是数组的快照,底层为一个结构体,包含长度、容量和指向数组的指针。切片赋值为结构体赋值,操作切片实质上是对数组指针的修改。值得注意的是,切片的更改会影响到所有引用其底层数组的切片实例。

       ### 切片扩容

       当在切片末尾添加元素时,切片长度加1。若长度未超过原数组容量,返回的仍为指向原数组的切片;否则返回指向新数组的切片,确保扩容后有足够的空间容纳新增元素。

       ### Defer机制

       Defer通过编译器将延迟执行的函数转化为汇编语言中的函数调用。首先,它将Defer函数及其参数入栈,形成链表结构,每个Defer在头部插入栈链表以保持执行顺序。在函数返回时,编译器插入代码执行栈链表中的Defer函数,先保存返回值,然后逐个执行Defer函数,最后返回最终的返回值。Go 1.对Defer进行了优化,将Defer操作直接移动到return操作的第二步,显著提高了执行效率。

       ### Map结构

       Map由桶数组组成,键值对通过哈希计算分组。Map内部分为桶外和桶内两部分:桶外通过数组映射,桶内通过高八位哈希值的数组进行查找。Map还包括B的扩容次数、溢出桶和哈希种子等重要组件。

       ### Map冲突处理

       在遇到哈希冲突时,Map在桶内寻找空位存储冲突的键值对。当桶内空间不足时,会使用溢出桶,类似于链表结构,遍历完当前桶后,再遍历溢出桶,确保所有元素被正确查找。

       ### Channel与阻塞协程

       Channel内部维护一个缓存环状线性链表,包含锁、容量、进出的索引值和阻塞的协程列表。Channel操作时需加锁,通过索引值判断元素进出,并利用阻塞协程列表迅速唤醒协程。

       ### Select实现

       Select本质上通过数组实现,存储不同类型的case(包括chan、操作类型和接受结构体),随机打乱顺序后逐一执行。当某个chan有元素时,执行取操作;反之,则将当前协程加入阻塞列表,直到有协程执行塞操作唤醒协程。

       ### Panic与recover

       Panic执行时,会先执行当前协程的defer列表和panic列表中的函数。使用recover恢复时,可恢复程序执行,但不会执行出错程序的下一行代码,而是回到recover函数所在函数的上一级函数。defer函数在当前函数返回时执行,即使函数中途panic,recover恢复后,返回值保持零值。

       ### unsafe.Pointer与uintptr

       unsafe.Pointer和uintptr类型可以相互转化。unsafe.Pointer可用于指针操作,但不能进行运算;uintptr支持运算,但不能转化为除unsafe.Pointer外的其他指针类型。两者常用于优化底层操作,如chan、map等结构的直接操作。

       ### 锁机制

       Go的锁机制涉及GMP调度模型,包括channel、select、time、网络IO、sync.Mutex、sync.Waitgroup、atomic等。channel和select通过维护阻塞协程列表实现高效并发控制。time和网络IO底层使用epoll实现多路复用。sync.Mutex、sync.Waitgroup和rwmutex等通过runtime的并发工具来实现锁的加锁和解锁。

       ### GMP模型与并发

       GMP模型中,协程(G)是用户定义的调度单位,内核线程(M)是操作系统最小调度单位,虚拟CPU(P)的数量一般等于CPU核数或倍数。GMP模型允许同时存在多协程,通过全局协程列表和P的协程队列进行调度。协程的切换比线程轻量,减少了系统开销。

       ### 反射原理

       反射通过reflect包提供访问对象类型、值及操作的动态能力。通过Type和Value方法识别接口类型,访问结构体字段,进行动态类型检查、赋值等操作。反射机制虽提供强大功能,但访问结构体字段时可能较慢,且频繁使用TypeOf和ValueOf可能导致GC压力增加。