go语言泛型实现原理,go解释型语言

如何看待go语言泛型的最新设计?

Go 由于不支持泛型而臭名昭著,但最近,泛型已接近成为现实。Go 团队实施了一个看起来比较稳定的设计草案,并且正以源到源翻译器原型的形式获得关注。本文讲述的是泛型的最新设计,以及如何自己尝试泛型。

创新互联是一家专注于网站制作、成都网站设计与策划设计,银州网站建设哪家好?创新互联做网站,专注于网站建设10年,网设计领域的专业建站公司;建站业务涵盖:银州等地区。银州做网站价格咨询:18982081108

例子

FIFO Stack

假设你要创建一个先进先出堆栈。没有泛型,你可能会这样实现:

type Stack []interface{}func (s Stack) Peek() interface{} {

return s[len(s)-1]

}

func (s *Stack) Pop() {

*s = (*s)[:

len(*s)-1]

}

func (s *Stack) Push(value interface{}) {

*s = 

append(*s, value)

}

但是,这里存在一个问题:每当你 Peek 项时,都必须使用类型断言将其从 interface{} 转换为你需要的类型。如果你的堆栈是 *MyObject 的堆栈,则意味着很多 s.Peek().(*MyObject)这样的代码。这不仅让人眼花缭乱,而且还可能引发错误。比如忘记 * 怎么办?或者如果您输入错误的类型怎么办?s.Push(MyObject{})` 可以顺利编译,而且你可能不会发现到自己的错误,直到它影响到你的整个服务为止。

通常,使用 interface{} 是相对危险的。使用更多受限制的类型总是更安全,因为可以在编译时而不是运行时发现问题。

泛型通过允许类型具有类型参数来解决此问题:

type Stack(type T) []Tfunc (s Stack(T)) Peek() T {

return s[len(s)-1]

}

func (s *Stack(T)) Pop() {

*s = (*s)[:

len(*s)-1]

}

func (s *Stack(T)) Push(value T) {

*s = 

append(*s, value)

}

这会向 Stack 添加一个类型参数,从而完全不需要 interface{}。现在,当你使用 Peek() 时,返回的值已经是原始类型,并且没有机会返回错误的值类型。这种方式更安全,更容易使用。(译注:就是看起来更丑陋,^-^)

此外,泛型代码通常更易于编译器优化,从而获得更好的性能(以二进制大小为代价)。如果我们对上面的非泛型代码和泛型代码进行基准测试,我们可以看到区别:

type MyObject struct {

int

}

var sink MyObjectfunc BenchmarkGo1(b *testing.B) {

for i := 0; i  b.N; i++ {

var s Stack

s.Push(MyObject{})

s.Push(MyObject{})

s.Pop()

sink = s.Peek().(MyObject)

}

}

func BenchmarkGo2(b *testing.B) {

for i := 0; i  b.N; i++ {

var s Stack(MyObject)

s.Push(MyObject{})

s.Push(MyObject{})

s.Pop()

sink = s.Peek()

}

}

结果:

BenchmarkGo1BenchmarkGo1-16     12837528         87.0 ns/op       48 B/op        2 allocs/opBenchmarkGo2BenchmarkGo2-16     28406479         41.9 ns/op       24 B/op        2 allocs/op

在这种情况下,我们分配更少的内存,同时泛型的速度是非泛型的两倍。

合约(Contracts)

上面的堆栈示例适用于任何类型。但是,在许多情况下,你需要编写仅适用于具有某些特征的类型的代码。例如,你可能希望堆栈要求类型实现 String() 函数

GO语言(十五):泛型入门(下)-

在本节中,您将添加通用函数调用的修改版本,进行小的更改以简化调用代码。您将删除在这种情况下不需要的类型参数。

当 Go 编译器可以推断您要使用的类型时,您可以在调用代码中省略类型参数。编译器从函数参数的类型推断类型参数。

请注意,这并不总是可能的。例如,如果您需要调用没有参数的泛型函数,则需要在函数调用中包含类型参数。

在 main.go 中,在您已有的代码下方,粘贴以下代码。

在此代码中:

(1)调用泛型函数,省略类型参数。

从包含 main.go 的目录中的命令行,运行代码。

接下来,您将通过将整数和浮点数的并集捕获到您可以重用的类型约束(例如从其他代码中)来进一步简化函数。

正如您将在本节中看到的,约束接口也可以引用特定类型。

1、编写代码

在此代码中:

b.在您已有的函数下方,粘贴以下通用 SumNumbers函数。

在此代码中:

c.在 main.go 中,在您已有的代码下方,粘贴以下代码。

在此代码中:

(1)调用SumNumbers打印每个map的总和。

与上一节一样,在调用泛型函数时省略了类型参数(方括号中的类型名称)。Go 编译器可以从其他参数推断类型参数。

从包含 main.go 的目录中的命令行,运行代码。

做得很好!您刚刚学习了 Go 中的泛型。

大家知道为什么golang不支持泛型

Golang团队认为在类型系统和运行时的复杂性花费太大,还没找到可以和这个复杂性相抵的良好设计。内置的map和slice其实都有泛型的味道,加上可以用interface{}来构造容器,可以达到泛型的效果。所以目前为止还没有直接的支持泛型。

【golang详解】go语言GMP(GPM)原理和调度

Goroutine调度是一个很复杂的机制,下面尝试用简单的语言描述一下Goroutine调度机制,想要对其有更深入的了解可以去研读一下源码。

首先介绍一下GMP什么意思:

G ----------- goroutine: 即Go协程,每个go关键字都会创建一个协程。

M ---------- thread内核级线程,所有的G都要放在M上才能运行。

P ----------- processor处理器,调度G到M上,其维护了一个队列,存储了所有需要它来调度的G。

Goroutine 调度器P和 OS 调度器是通过 M 结合起来的,每个 M 都代表了 1 个内核线程,OS 调度器负责把内核线程分配到 CPU 的核上执行

模型图:

避免频繁的创建、销毁线程,而是对线程的复用。

1)work stealing机制

当本线程无可运行的G时,尝试从其他线程绑定的P偷取G,而不是销毁线程。

2)hand off机制

当本线程M0因为G0进行系统调用阻塞时,线程释放绑定的P,把P转移给其他空闲的线程执行。进而某个空闲的M1获取P,继续执行P队列中剩下的G。而M0由于陷入系统调用而进被阻塞,M1接替M0的工作,只要P不空闲,就可以保证充分利用CPU。M1的来源有可能是M的缓存池,也可能是新建的。当G0系统调用结束后,根据M0是否能获取到P,将会将G0做不同的处理:

如果有空闲的P,则获取一个P,继续执行G0。

如果没有空闲的P,则将G0放入全局队列,等待被其他的P调度。然后M0将进入缓存池睡眠。

如下图

GOMAXPROCS设置P的数量,最多有GOMAXPROCS个线程分布在多个CPU上同时运行

在Go中一个goroutine最多占用CPU 10ms,防止其他goroutine被饿死。

具体可以去看另一篇文章

【Golang详解】go语言调度机制 抢占式调度

当创建一个新的G之后优先加入本地队列,如果本地队列满了,会将本地队列的G移动到全局队列里面,当M执行work stealing从其他P偷不到G时,它可以从全局G队列获取G。

协程经历过程

我们创建一个协程 go func()经历过程如下图:

说明:

这里有两个存储G的队列,一个是局部调度器P的本地队列、一个是全局G队列。新创建的G会先保存在P的本地队列中,如果P的本地队列已经满了就会保存在全局的队列中;处理器本地队列是一个使用数组构成的环形链表,它最多可以存储 256 个待执行任务。

G只能运行在M中,一个M必须持有一个P,M与P是1:1的关系。M会从P的本地队列弹出一个可执行状态的G来执行,如果P的本地队列为空,就会想其他的MP组合偷取一个可执行的G来执行;

一个M调度G执行的过程是一个循环机制;会一直从本地队列或全局队列中获取G

上面说到P的个数默认等于CPU核数,每个M必须持有一个P才可以执行G,一般情况下M的个数会略大于P的个数,这多出来的M将会在G产生系统调用时发挥作用。类似线程池,Go也提供一个M的池子,需要时从池子中获取,用完放回池子,不够用时就再创建一个。

work-stealing调度算法:当M执行完了当前P的本地队列队列里的所有G后,P也不会就这么在那躺尸啥都不干,它会先尝试从全局队列队列寻找G来执行,如果全局队列为空,它会随机挑选另外一个P,从它的队列里中拿走一半的G到自己的队列中执行。

如果一切正常,调度器会以上述的那种方式顺畅地运行,但这个世界没这么美好,总有意外发生,以下分析goroutine在两种例外情况下的行为。

Go runtime会在下面的goroutine被阻塞的情况下运行另外一个goroutine:

用户态阻塞/唤醒

当goroutine因为channel操作或者network I/O而阻塞时(实际上golang已经用netpoller实现了goroutine网络I/O阻塞不会导致M被阻塞,仅阻塞G,这里仅仅是举个栗子),对应的G会被放置到某个wait队列(如channel的waitq),该G的状态由_Gruning变为_Gwaitting,而M会跳过该G尝试获取并执行下一个G,如果此时没有可运行的G供M运行,那么M将解绑P,并进入sleep状态;当阻塞的G被另一端的G2唤醒时(比如channel的可读/写通知),G被标记为,尝试加入G2所在P的runnext(runnext是线程下一个需要执行的 Goroutine。), 然后再是P的本地队列和全局队列。

系统调用阻塞

当M执行某一个G时候如果发生了阻塞操作,M会阻塞,如果当前有一些G在执行,调度器会把这个线程M从P中摘除,然后再创建一个新的操作系统的线程(如果有空闲的线程可用就复用空闲线程)来服务于这个P。当M系统调用结束时候,这个G会尝试获取一个空闲的P执行,并放入到这个P的本地队列。如果获取不到P,那么这个线程M变成休眠状态, 加入到空闲线程中,然后这个G会被放入全局队列中。

队列轮转

可见每个P维护着一个包含G的队列,不考虑G进入系统调用或IO操作的情况下,P周期性的将G调度到M中执行,执行一小段时间,将上下文保存下来,然后将G放到队列尾部,然后从队列中重新取出一个G进行调度。

除了每个P维护的G队列以外,还有一个全局的队列,每个P会周期性地查看全局队列中是否有G待运行并将其调度到M中执行,全局队列中G的来源,主要有从系统调用中恢复的G。之所以P会周期性地查看全局队列,也是为了防止全局队列中的G被饿死。

除了每个P维护的G队列以外,还有一个全局的队列,每个P会周期性地查看全局队列中是否有G待运行并将其调度到M中执行,全局队列中G的来源,主要有从系统调用中恢复的G。之所以P会周期性地查看全局队列,也是为了防止全局队列中的G被饿死。

M0

M0是启动程序后的编号为0的主线程,这个M对应的实例会在全局变量rutime.m0中,不需要在heap上分配,M0负责执行初始化操作和启动第一个G,在之后M0就和其他的M一样了

G0

G0是每次启动一个M都会第一个创建的goroutine,G0仅用于负责调度G,G0不指向任何可执行的函数,每个M都会有一个自己的G0,在调度或系统调用时会使用G0的栈空间,全局变量的G0是M0的G0

一个G由于调度被中断,此后如何恢复?

中断的时候将寄存器里的栈信息,保存到自己的G对象里面。当再次轮到自己执行时,将自己保存的栈信息复制到寄存器里面,这样就接着上次之后运行了。

我这里只是根据自己的理解进行了简单的介绍,想要详细了解有关GMP的底层原理可以去看Go调度器 G-P-M 模型的设计者的文档或直接看源码

参考: ()

()

尝试用golang 1.18泛型实现orm

这几天golang社区对泛型的讨论非常多的,一片热火朝天的景象。对我们广大gopher来说总归是好事。

泛型很有可能会颠覆我们之前的很多设计,带着这种疑问和冲动,我准备尝试用golang泛型实现几个orm的常见功能。

本文并没完全实现通用的orm,只是探讨其实现的一种方式提供各位读者做借鉴。

虽然golang有了泛型,但是目前在标准库sql底层还没有改造,目前还有很多地方需要用到reflect。

调用方式

这个部分跟传统的orm使用上没有太大区别,没办法不使用反射的情况下,泛型的方式可能变得有点繁琐。

调用方式

和创建table类似,写入数据好像比没有之前的orm有优势。

读取数据是非常高频的操作,所以我们稍作封装。

调用方式

稍微比原先的orm方式有了多一点想象空间,比如 在[T any]做更明确的约束,比如要求实现Filter定制方法。

鉴于本人能力还认证有限,目前还没有发现泛型对orm剧烈的改进和突破的可能。未来如果go对底层sql做出改动,或者实现诸如Rust那种Enum方式,可能会带来更多的惊喜。

为什么要使用 Go 语言?Go 语言的优势在哪里?

1、简单易学。

Go语言的作者本身就很懂C语言,所以同样Go语言也会有C语言的基因,所以对于程序员来说,Go语言天生就会让人很熟悉,容易上手。

2、并发性好。

Go语言天生支持并发,可以充分利用多核,轻松地使用并发。 这是Go语言最大的特点。

描述

Go的语法接近C语言,但对于变量的声明有所不同。Go支持垃圾回收功能。Go的并行模型是以东尼·霍尔的通信顺序进程(CSP)为基础,采取类似模型的其他语言包括Occam和Limbo,但它也具有Pi运算的特征,比如通道传输。

在1.8版本中开放插件(Plugin)的支持,这意味着现在能从Go中动态加载部分函数。

与C++相比,Go并不包括如枚举、异常处理、继承、泛型、断言、虚函数等功能,但增加了 切片(Slice) 型、并发、管道、垃圾回收、接口(Interface)等特性的语言级支持。


网站题目:go语言泛型实现原理,go解释型语言
当前网址:http://azwzsj.com/article/hcsdsp.html