goroutine VS python 协程

协程概念 协程,其实可以理解为一种特殊的程序调用。特殊的是在执行过程中,在子程序(或者说函数)内部可中断,然后转而执行别的子程序,在适当的时候再返回来接着执行。 它有两个特征: 可中断,这里的中断不是普通的函数调用,而是类似CPU的中断,CPU在这里直接释放转到其他程序断点继续执行。 可恢复,等到合适的时候,可以恢复到中断的地方继续执行。 和进程线程的区别 上面两个特点就导致了它相对于线程和进程切换来说极高的执行效率,为什么这么说呢?我们先老生常谈地说一下进程和线程。 进程是操作系统资源分配的基本单位,线程是操作系统调度和执行的最小单位。 这两句应该是我们最常听到的两句话,拆开来说, 进程是程序的启动实例,拥有代码和打开的文件资源、数据资源、独立的内存空间。 线程从属于进程,是程序的实际执行者,一个进程至少包含一个主线程,也可以有更多的子线程,线程拥有自己的栈空间。无论是进程还是线程,都是由操作系统所管理和切换的。 我们再来看协程,它又叫做微线程,但其实它和进程还有线程完全不是一个维度上的概念。 进程和线程的切换完全是用户无感,由操作系统控制,从用户态到内核态再到用户态。 而协程的切换完全是程序代码控制的,在用户态的切换,就像函数回调的消耗一样,在线程的栈内即可完成。 ...

3 min · 1177 words · Luenci

Golang的goroutine模型概述和调度

G-P-M 模型概述 每一个OS线程都有一个固定大小的内存块(一般会是2MB)来做栈,这个栈会用来存储当前正在被调用或挂起(指在调用其它函数时)的函数的内部变量。 这个固定大小的栈同时很大又很小。因为2MB的栈对于一个小小的Goroutine来说是很大的内存浪费,而对于一些复杂的任务(如深度嵌套的递归)来说又显得太小。因此,Go语言做了它自己的『线程』。 在Go语言中,每一个Goroutine是一个独立的执行单元,相较于每个OS线程固定分配2M内存的模式,Goroutine的栈采取了动态扩容方式, 初始时仅为2KB,随着任务执行按需增长,最大可达1GB(64位机器最大是1G,32位机器最大是256M),且完全由Golang自己的调度器 Go Scheduler 来调度。 此外,GC还会周期性地将不再使用的内存回收,收缩栈空间。 因此,Go程序可以同时并发成千上万个Goroutine是得益于它强劲的调度器和高效的内存模型。 任何用户线程最终肯定都是要交由OS线程来执行 ​ Goroutine(称为G)也不例外,但是G并不直接绑定OS线程运行,而是由Goroutine Scheduler中的 P - Logical Processor (逻辑处理器)来作为两者的『中介』。 P 可以看作是一个抽象的资源或者一个上下文,一个P绑定一个OS线程,在Golang的实现里把OS线程抽象成一个数据结构。 M,G实际上是由M通过P来进行调度运行的,但是在G的层面来看,P提供了G运行所需的一切资源和环境,因此在G看来P就是运行它的 “CPU”,由 G、P、M 这三种由Go抽象出来的实现,最终形成了Go调度器的基本结构: G: Goroutine G有以下状态 非GC状态 idle:_Gidle for idle,意思是这个goroutine刚被创建出来,还未被进行初始化。 runnable: _Grunnable for runnable意思是这个goroutine已经在运行队列,在这种情况下,goroutine还未执行用户代码,M的执行栈还不是goroutine自己的 running: _Grunning for running,意思是goroutine可能正在执行用户代码,M的执行栈已经由该goroutine所拥有,此时对象g不在运行队列中。这个状态值要待分配给M和P之后,交由M和P来设定 syscall, waiting, dead, copystack 对应的GC状态 scan, scanrunnable, scan running, scansyscall, scanwaiting _Gscan系列,用于标记正在被GC扫描的状态,这些状态是由_Gscan=0x1000再加上_GRunnable, _Grunning, _Gsyscall和_Gwaiting的枚举值所产生的,这么做的好处是直接通过简单的运算即可知道被Scan之前的状态。当被标记为这系列的状态时,这些goroutine都不会执行用户代码,并且它们的执行栈都是被做该GC的goroutine所拥有。不过_Gscanrunning状态有点特别,这个标记是为了阻止正在运行的goroutine切换成其它状态,并告诉这个G自己扫描自己的堆栈。正是这种巧妙的方式,使得Go语言的GC十分高效。 每个Goroutine对应一个G结构体,G 存储 Goroutine的运行堆栈、状态以及任务函数,可重用。 G并非执行体,每个G需要绑定到P才能被调度执行。 P: Processor 表示逻辑处理器, 对G来说,P相当于CPU核,G只有绑定到P(在P的local run中)才能被调度。对M来说,P提供了相关的执行环境(Context),如内存分配状态(mcache),任务队列(G)等,P的数量决定了系统内最大可并行的G的数量(前提:物理CPU核数 >= P的数量),P的数量由用户设置的GoMAXPROCS决定,但是不论GoMAXPROCS设置为多大,P的数量最大为256。 golang runtime是有个sysmon的协程,他会轮询的检测所有的P上下文队列,**只要 G-M 的线程长时间在阻塞状态,那么就重新创建一个线程去从runtime P队列里获取任务。先前的阻塞的线程会被游离出去了,当他完成阻塞操作后会触发相关的callback回调,并加入回线程组里。**简单说,如果你没有特意配置runtime.SetMaxThreads,那么在没有可复用的线程的情况下,会一直创建新线程。 M: Machine ​ OS线程抽象,代表着真正执行计算的资源。 在绑定有效的P后,进入schedule循环;而schedule循环的机制大致是从Global队列、P的Local队列以及wait队列中获取G,切换到G的执行栈上并执行G的函数,调用Goexit做清理工作并回到M,如此反复。 M并不保留G状态,这是G可以跨M调度的基础,M的数量是不定的,由Go Runtime调整,为了防止创建过多OS线程导致系统调度不过来,目前默认最大限制为10000个。 在绝大多数时候,其实P的数量和M的数量是相等。 每创建一个p, 就会创建一个对应的M只有少数情况下,M的数量会大于P work-stealing 的调度算法 每个P维护一个G的本地队列; 当一个G被创建出来,或者变为可执行状态时,就把他放到P的可执行队列中; 当一个G在M里执行结束后,P会从队列中把该G取出;如果此时P的队列为空,即没有其他G可以执行, M就随机选择另外一个P,从其可执行的G队列中取走一半。 ...

7 min · 3138 words · Luenci

高并发,高可用,高性能

原文链接:https://juejin.cn/post/6844903944955625479 一、高并发 简介 高并发(High Concurrency)是互联网分布式系统架构设计中必须考虑的因素之一,它通常是指,通过设计保证系统能够同时并行处理很多请求。 高并发相关常用的一些指标有响应时间(Response Time),吞吐量(Throughput),每秒查询率QPS(Query Per Second),并发用户数等。 响应时间:系统对请求做出响应的时间。例如系统处理一个HTTP请求需要200ms,这个200ms就是系统的响应时间。 吞吐量:单位时间内处理的请求数量。 QPS:每秒响应请求数。在互联网领域,这个指标和吞吐量区分的没有这么明显。 并发用户数:同时承载正常使用系统功能的用户数量。例如一个即时通讯系统,同时在线量一定程度上代表了系统的并发用户数。 如何提高并发能力 垂直扩展 (Scale Up) 增强单机硬件性能(优先):例如:增加CPU核数如32核,升级更好的网卡如万兆,升级更好的硬盘如SSD,扩充硬盘容量如2T,扩充系统内存如128G。 提升单机架构性能:例如:使用Cache来减少IO次数,使用异步来增加单服务吞吐量,使用无锁数据结构来减少响应时间。 总结:管是提升单机硬件性能,还是提升单机架构性能,都有一个致命的不足:单机性能总是有极限的。所以互联网分布式架构设计高并发终极解决方案还是水平扩展。 水平扩展 (Scale Out) 只要增加服务器数量,就能线性扩充系统性能。水平扩展对系统架构设计是有要求的,难点在于:如何在架构各层进行可水平扩展的设计。 二、高性能 简介 简单的说,高性能(High Performance)就是指程序处理速度快,所占内存少,cpu占用率低。 高并发和高性能是紧密相关的,提高应用的性能,是肯定可以提高系统的并发能力的。 应用性能优化的时候,对于计算密集型和IO密集型还是有很大差别,需要分开来考虑。 增加服务器资源(CPU、内存、服务器数量),绝大部分时候是可以提高应用的并发能力和性能 (前提是应用能够支持多任务并行计算,多服务器分布式计算才行),但也是要避免其中的一些问题,才可以更好的更有效率的利用服务器资源。 提高性能的注意事项 避免因为IO阻塞让CPU闲置,导致CPU的浪费。 避免多线程间增加锁来保证同步,导致并行系统串行化。 免创建、销毁、维护太多进程、线程,导致操作系统浪费资源在调度上。 避免分布式系统中多服务器的关联,比如:依赖同一个mysql,程序逻辑中使用分布式锁,导致瓶颈在mysql,分布式又变成串行化运算。 ...

4 min · 1624 words · Luenci

初探架构设计

架构设计的主要目的是为了解决软件系统复杂度带来的问题 架构、框架、组件、模块、系统 OLAP(Online Analytical Processing)在线分析处理 架构是顶层设计;框架是面向编程或配置的半成品;组件是从技术维度上的复用;模块是从业务维度上职责的划分;系统是相互协同可运行的实体。 架构设计的目的 明确“架构设计是为了解决软件复杂度”原则后 “这么多需求,从哪里开始下手进行架构设计呢?” — 通过熟悉和理解需求,识别系统复杂性所在的地方,然后针对这些复杂点进行架构设计。 “架构设计要考虑高性能、高可用、高扩展……这么多高 XX,全部设计完成估计要 1 个月,但老大只给了 1 周时间” —架构设计并不是要面面俱到,不需要每个架构都具备高性能、高可用、高扩展等特点,而是要识别出复杂点然后有针对性地解决问题。 “业界 A 公司的架构是 X,B 公司的方案是 Y,两个差别比较大,该参考哪一个呢?” —理解每个架构方案背后所需要解决的复杂点,然后才能对比自己的业务复杂点,参考复杂点相似的方案。 其次,遵循这条准则能够让“老鸟”架构师有的放矢,而不是贪大求全。技术人员往往都希望自己能够做出最牛的东西,架构师也不例外,尤其是一些“老鸟”架构师,为了证明自己的技术牛,可能会陷入贪大求全的焦油坑而无法自拔。 例如:“我们的系统一定要做到每秒 TPS 10 万”。 “淘宝的架构是这么做的,我们也要这么做”。 “Docker 现在很流行,我们的架构应该将 Docker 应用进来”。 以上这些想法,如果拿“架构设计是为了解决软件复杂度”这个原则来衡量,就很容易判断。 “我们的系统一定要做到每秒 TPS 10 万” —如果系统的复杂度不是在性能这部分,TPS 做到 10 万并没有什么用。 “淘宝的架构是这么做的,我们也要这么做” —淘宝的架构是为了解决淘宝业务的复杂度而设计的,淘宝的业务复杂度并不就是我们的业务复杂度,绝大多数业务的用户量都不可能有淘宝那么大。 “Docker 现在很流行,我们的架构应该将 Docker 应用进来 —Docker 不是万能的,只是为了解决资源重用和动态分配而设计的,如果我们的系统复杂度根本不是在这方面,引入 Docker 没有什么意义。 ...

8 min · 3769 words · Luenci

golang常见错误之defer和迭代

defer 解析 作用域 向 defer 关键字传入的函数会在函数返回之前运行。假设我们在 for 循环中多次调用 defer 关键字: 1 2 3 4 5 6 7 8 9 10 11 12 func main() { for i := 0; i < 5; i++ { defer fmt.Println(i) } } $ go run main.go 4 3 2 1 0 运行上述代码会倒序执行传入 defer 关键字的所有表达式,因为最后一次调用 defer 时传入了 fmt.Println(4),所以这段代码会优先打印 4。我们可以通过下面这个简单例子强化对 defer 执行时机的理解: ...

4 min · 1704 words · Luenci

golang并发编程

预备知识 unsafe.Pointer unsafe.Pointer 是一种特殊意义的指针,它可以包含任意类型的地址,有点类似于 C 语言里的 void* 指针,全能型的。 对unsafe.Pointer 又爱又恨,你会有效使用它吗? unsafe 是关注 Go 程序操作类型安全的包。 unsafe.Pointer 可以让你无视 Go 的类型系统,完成任何类型与内建的 uintptr 类型之间的转化。根据文档,unsafe.Pointer 可以实现四种其他类型不能的操作: 任何类型的指针都可以转化为一个 unsafe.Pointer 一个 unsafe.Pointer 可以转化成任何类型的指针 一个 uintptr 可以转化成一个 unsafe.Pointer 一个 unsafe.Pointer 可以转化成一个 uintptr 两种只能借助 unsafe 包才能完成的操作: 使用 unsafe.Pointer 实现两种类型间转换 使用 unsafe.Pointer 处理系统调用。 CAS比较并交换—-Compare And Swap Go 的一个 CAS 操作使用场景 在并发执行的多个 routine R1,R2…Rn 的中,同一时间只允许唯一一个 routine 执行某一个操作,并且其他 routine 需要非阻塞的知道自己无权操作并返回的时候,可以使用 CAS 操作。 大方向:任务编排用 Channel,共享资源保护用传统并发原语 互斥锁实现机制 使用互斥锁,限定临界区只能同时由一个线程持有。 临界区 在并发编程中,如果程序中的一部分会被并发访问或修改,那么,为了避免并发访问导致的意想不到的结果,这部分程序需要被保护起来,这部分被保护起来的程序,就叫做临界区。 在 Go 标准库中,它提供了 Mutex 来实现互斥锁这个功能。 共享资源。并发地读写共享资源,会出现数据竞争(data race)的问题,所以需要 Mutex、RWMutex 这样的并发原语来保护。 任务编排。需要 goroutine 按照一定的规律执行,而 goroutine 之间有相互等待或者依赖的顺序关系,我们常常使用 WaitGroup 或者 Channel 来实现。 消息传递。信息交流以及不同的 goroutine 之间的线程安全的数据交流,常常使用 Channel 来实现。 ...

22 min · 10919 words · Luenci

golang中的nil

深入理解golang中的nil nil is (a) zero 什么是零值(zero value) 1 2 3 4 5 6 7 8 9 10 11 12 // go中的零值 bool -> false numbers -> 0 string -> "" pointers -> nil // point to nothing slices -> nil // have no backing array maps -> nil // are not initialized channels -> nil // are not initialized functions -> nil // are not initialized interfaces -> nil // have no value assigned, not even a nil pointer struct中的零值(zero values) 1 2 3 4 5 6 7 type Person struct{ Age int Name string Friend []Person } var p Person // Person{0,"",nil} nil 是什么类型(type) “nil is a predeclared identifier in Go that represents zero values for pointers, interfaces, channels, maps, slices and function types.” ...

2 min · 574 words · Luenci

云计算和云原生概念浅析

云计算定义 一种能够跨网络、按需提供基础架构、服务、平台和应用的交付方式,正在快速取代原本通过硬布线连接进行资源共享的方式。 – redhat 云计算解决的问题主要是物理资源上云,通过虚拟化技术来将底层资源池化,达到弹性、可控等目的。然而大多数传统应用并不是面向云环境来构建的,这里面包含了大量开发需求(开发框架、类库、后段服务等),就导致了云端的强大能力没有被完全发挥出来。因此,摒弃传统的应用技术架构,基于云的特点重新构建云原生应用,成为企业上云的下一个阶段。 特点 云计算是指在云中运行工作负载,而云是一种能够抽象、汇集和共享整个网络中的可扩展资源的 IT 环境。云计算和云本身都不属于技术的范畴。 云计算是指在云中运行工作负载的功能。 而云是一种环境,是运行应用的地方。 技术则是指用于构建和使用云的软件和硬件 云计算为云原生提供了物理基础,做个不太精确的比喻:“云计算是电脑硬件,云原生是应用”. ...

9 min · 4295 words · Luenci

Golang指针详解

Golang指针 原文来自:https://www.cnblogs.com/-wenli/p/12682477.html *类型:普通指针类型,用于传递对象地址,不能进行指针运算。 unsafe.Pointer:通用指针类型,用于转换不同类型的指针,不能进行指针运算,不能读取内存存储的值(必须转换到某一类型的普通指针)。 uintptr:用于指针运算,GC 不把 uintptr 当指针,uintptr 无法持有对象。uintptr 类型的目标会被回收。 unsafe.Pointer 是桥梁,可以让任意类型的指针实现相互转换,也可以将任意类型的指针转换为 uintptr 进行指针运算。 unsafe.Pointer 不能参与指针运算,比如你要在某个指针地址上加上一个偏移量,Pointer是不能做这个运算的,那么谁可以呢? 就是uintptr类型了,只要将Pointer类型转换成uintptr类型,做完加减法后,转换成Pointer,通过*操作,取值,修改值,随意。 小结 unsafe.Pointer 可以让你的变量在不同的普通指针类型转来转去,也就是表示为任意可寻址的指针类型。而 uintptr 常用于与 unsafe.Pointer 打配合,用于做指针运算。 unsafe.Pointer unsafe 是关注 Go 程序操作类型安全的包。 unsafe.Pointer 可以让你无视 Go 的类型系统,完成任何类型与内建的 uintptr 类型之间的转化。 unsafe.Pointer称为通用指针,官方文档对该类型有四个重要描述: (1)任何类型的指针都可以被转化为Pointer (2)Pointer可以被转化为任何类型的指针 (3)uintptr可以被转化为Pointer (4)Pointer可以被转化为uintptr unsafe.Pointer是特别定义的一种指针类型(译注:类似C语言中的void类型的指针),在golang中是用于各种指针相互转换的桥梁,它可以包含任意类型变量的地址。 当然,我们不可以直接通过*p来获取unsafe.Pointer指针指向的真实变量的值,因为我们并不知道变量的具体类型。 和普通指针一样,unsafe.Pointer指针也是可以比较的,并且支持和nil常量比较判断是否为空指针。 uintptr uintptr是一个整数类型。 1 2 3 // uintptr is an integer type that is large enough to hold the bit pattern of // any pointer. type uintptr uintptr 即使uintptr变量仍然有效,由uintptr变量表示的地址处的数据也可能被GC回收,这个需要注意! unsafe包 unsafe包只有两个类型,三个函数,但是功能很强大。 1 2 3 4 5 type ArbitraryType int type Pointer *ArbitraryType func Sizeof(x ArbitraryType) uintptr func Offsetof(x ArbitraryType) uintptr func Alignof(x ArbitraryType) uintptr ArbitraryType是int的一个别名,在Go中对ArbitraryType赋予特殊的意义。代表一个任意Go表达式类型。 Pointer是int指针类型的一个别名,在Go中可以把Pointer类型,理解成任何指针的父类型。 三个函数的参数均是ArbitraryType类型,就是接受任何类型的变量。 unsafe.Sizeof 接受任意类型的值(表达式),返回其占用的字节数,这和c语言里面不同,c语言里面sizeof函数的参数是类型,而这里是一个表达式,比如一个变量。 unsafe.Offsetof:返回结构体中元素所在内存的偏移量。 Alignof 返回变量对齐字节数量Offsetof返回变量指定属性的偏移量,这个函数虽然接收的是任何类型的变量,但是有一个前提,就是变量要是一个struct类型,且还不能直接将这个struct类型的变量当作参数,只能将这个struct类型变量的属性当作参数。 ...

8 min · 3728 words · Luenci

golang的组合和嵌套(面向接口编程)

官方关于golang的继承和重载的FAQ 原文部分来自:https://segmentfault.com/a/1190000022429780 关于类型继承 面向对象的编程,至少在最著名的语言中,涉及对类型之间关系的过多讨论,这些关系通常可以自动派生。Go 采取了不同的方法。 与其要求程序员提前声明两种类型是相关的,在 Go 中,类型会自动满足任何指定其方法子集的接口。除了减少簿记之外,这种方法还有真正的优势。类型可以同时满足多个接口,没有传统多重继承的复杂性。接口可以是非常轻量级的——具有一个甚至零个方法的接口可以表达一个有用的概念。如果出现新想法或用于测试,可以事后添加接口——无需注释原始类型。因为类型和接口之间没有明确的关系,所以没有要管理或讨论的类型层次结构。 可以使用这些想法来构建类似于类型安全的 Unix 管道的东西。例如,了解如何fmt.Fprintf 为任何输出启用格式化打印,而不仅仅是文件,或者bufio包如何与 文件 I/O 完全分离,或者image包如何生成压缩图像文件。所有这些想法都源于io.Writer表示单个方法 ( Write)的单个接口( )。而这只是皮毛。Go 的接口对程序的结构有着深远的影响。 这需要一些时间来适应,但这种隐式的类型依赖是 Go 最高效的事情之一。 ​ –faq: https://golang.org/doc/faq#inheritance 关于重载的定义 如果不需要进行类型匹配,则方法分派会得到简化。使用其他语言的经验告诉我们,拥有多种名称相同但签名不同的方法有时很有用,但在实践中也可能会令人困惑和脆弱。仅按名称匹配并要求类型的一致性是 Go 类型系统中一个主要的简化决定。 关于运算符重载,它似乎更方便而不是绝对要求。同样,没有它,事情会更简单。 ​ – faq:https://golang.org/doc/faq#overloading 从一个案例引入 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 type ShapeInterface interface { Area() float64 GetName() string PrintArea() } type Shape struct { name string } func (s *Shape) GetName() string { return s.name } func (s *Shape) Area() float64 { return 0.0 } func (s *Shape) PrintArea() { fmt.Printf("%s : Area %v\r\n", s.GetName(), s.Area()) } // Rectangle 矩形求面积 type Rectangle struct { Shape w, h float64 } func (r *Rectangle) Area() float64 { return r.w * r.h } // Circle 圆形 : 重新定义 Area 和PrintArea 方法 type Circle struct { Shape r float64 } func (c *Circle) Area() float64 { return c.r * c.r * math.Pi } func (c *Circle) PrintArea() { fmt.Printf("%s : Area %v\r\n", c.GetName(), c.Area()) } func main() { s := Shape{name: "Shape"} c := Circle{Shape: Shape{name: "Circle"}, r: 10} r := Rectangle{Shape: Shape{name: "Rectangle"}, w: 5, h: 4} listshape := []ShapeInterface{&s, &c, &r} for _, si := range listshape { si.PrintArea() //!! 猜猜哪个Area()方法会被调用 !! } } out: Shape : Area 0 Circle : Area 314.1592653589793 Rectangle : Area 0 // 为啥这里没有调用 5 * 4 原因分析:Rectangle通过组合Shape获得的PrintArea()方法并没有去调用Rectangle实现的Area()方法,而是去调用了Shape的Area()方法。Circle是因为自己重写了PrintArea()所以在方法里调用到了自身的Area()。 ...

5 min · 2140 words · Luenci

Context 正确使用姿势(推荐)

Context 正确使用姿势 原文参考:https://juejin.cn/post/6844903929340231694 Context 是 immutable(不可变的) context.Context API 基本上是两类操作: 3个函数用于限定什么时候你的子节点退出; 1个函数用于设置请求范畴的变量 1 2 3 4 5 6 7 8 type Context interface { // 啥时候退出 Deadline() (deadline time.Time, ok bool) Done() <-chan struct{} Err() error // 设置变量 Value(key interface{}) interface{} } 如何创建 Context? 在 RPC 开始的时候,使用 context.Background() 有些人把在 main() 里记录一个 context.Background(),然后把这个放到服务器的某个变量里,然后请求来了后从这个变量里继承 context。这么做是不对的。直接每个请求,源自自己的 context.Background() 即可。 如果你没有 context,却需要调用一个 context 的函数的话,用 context.TODO() 如果某步操作需要自己的超时设置的话,给它一个独立的 sub-context(如前面的例子) Context 放哪? 把 Context 想象为一条河流流过你的程序 理想情况下,Context 存在于调用栈(Call Stack) 中 不要把 Context 存储到一个 struct 里 除非你使用的是像 http.Request 中的 request 结构体的方式 request 结构体应该以 Request 结束为生命终止 当 RPC 请求处理结束后,应该去掉对 Context 变量的引用(Unreference) Request 结束,Context 就应该结束。 Context 包的注意事项 要养成关闭 Context 的习惯 特别是 超时的 Contexts 如果一个 context 被 GC 而不是 cancel 了,那一般是你做错了 1 2 ctx, cancel := context.WithTimeout(parentCtx, time.Second * 2) defer cancel()、 使用 Timeout 会导致内部使用 time.AfterFunc,从而会导致 context 在计时器到时之前都不会被垃圾回收。 在建立之后,立即 defer cancel() 是一个好习惯。 ...

7 min · 3432 words · Luenci

聊聊 Wire 依赖注入

Wire 等依赖注入工具旨在简化初始化代码的管理 💡 依赖注入 是一种标准技术,用于生成灵活且松散耦合的代码,通过显式地为组件提供它们工作所需的所有依赖项。 Wire 介绍 参考文章:Golang依赖注入框架wire使用详解_魂影魔宅-CSDN博客_golang wire Wire 有两个基本概念:提供者(provider)和注入器(Injector)。 官方的定义: provider: a function that can produce a value. These functions are ordinary Go code. injector: a function that calls providers in dependency order. With Wire, you write the injector’s signature, then Wire generates the function’s body. 提供者(provider):一个可以产生值的函数(通常是构造函数)。这些函数都是普通的 Go 代码 注入器(Injector):一个按依赖顺序调用提供者的函数。 使用 Wire,您编写注入器(Injector)的签名,然后 Wire 生成函数的主体。 ...

5 min · 2456 words · Luenci

defer的妙用之跟踪函数调用链

defer的妙用之跟踪函数调用链 本文参考:Tony Bai 老师的极客时间 《Tony Bai · Go语言第一课》 课程 使用 defer 可以跟踪函数的 执行过程 defer 会预计算参数(表达式进行求值): 详情见 https://luenci.com 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 // trace.go package main func Trace(name string) func() { println("enter:", name) return func() { println("exit:", name) } } func foo() { defer Trace("foo")() bar() } func bar() { defer Trace("bar")() } func main() { defer Trace("main")() foo() } out: enter: main enter: foo enter: bar exit: bar exit: foo exit: main 程序的函数调用的全过程一目了然地展现在了我们面前:程序按 main -> foo -> bar的函数调用次序执行,代码在函数的入口与出口处分别输出了跟踪日志。 Go 会对 defer 后面的表达式Trace("foo")()进行求值。由于这个表达式包含一个函数调用Trace("foo"),所以这个函数会被执行。 程序存在的问题 调用 Trace 时需手动显式传入要跟踪的函数名; 如果是并发应用,不同 Goroutine 中函数链跟踪混在一起无法分辨; 输出的跟踪结果缺少层次感,调用关系不易识别; 对要跟踪的函数,需手动调用 Trace 函数 接下来我们一步一步的解决这些问题,来完善我们的函数调用链。 ...

6 min · 2516 words · Luenci

Golang 并发模型 & Goroutine 详解

Golang 并发模型 & Goroutine 详解 💡 并发不是并行,并发关乎结构,并行关乎执行 操作系统的基本调度与执行单元是进程(process) 操作系统的最小调度单位是线程-线程可作为执行单元可被独立调度到处理器上运行 **CSP( Communicationing Sequential Processes,通信顺序进程)**并发模型 Tony Hoare 的 CSP 模型旨在简化并发程序的编写,让并发程序的编写与编写顺序程序一样简单。Tony Hoare 认为输入输出应该是基本的编程原语,数据处理逻辑(也就是 CSP中的 P)只需调用输入原语获取数据,顺序地处理数据,并将结果数据通过输出原语输出就可以了。因此,在 Tony Hoare 眼中,一个符合 CSP 模型的并发程序应该是一组通过输入输出原语连接起来的 P 的集合。 从这个角度来看,CSP理论不仅是一个并发参考模型,也是一种并发程序的程序组织方法。它的组合思想与 Go 的设计哲学不谋而合。 Tony Hoare 的 CSP 理论中的 P,也就是“Process(进程)”,是一个抽象概念,它代表任何顺序处理逻辑的封装,它获取输入数据(或从其他 P 的输出获取),并生产出可以被其他 P 消费的输出数据。这里我们可以简单看下 CSP 通信模型的示意图: 注意了,这里的 P 并不一定与操作系统的进程或线程划等号。在 Go 中,与“Process”对 应的是 goroutine。 为了实现 CSP 并发模型中的输入和输出原语,Go 还引入了 goroutine(P)之间的通信原语channel。goroutine 可以从 channel 获取输入数据, 再将处理后得到的结果数据通过 channel 输出。通过 channel 将 goroutine(P)组合连 接在一起,让设计和编写大型并发系统变得更加简单和清晰,我们再也不用为那些传统共 享内存并发模型中的问题而伤脑筋了。 Goroutine 的优势 资源占用小,每个 goroutine 的初始栈大小仅为 2k; 由 Go 运行时而不是操作系统调度,goroutine 上下文切换在用户层完成,开销更小; 在语言层面而不是通过标准库提供。goroutine 由go关键字创建,一退出就会被回收或 销毁,开发体验更佳 语言内置 channel 作为 goroutine 间通信原语,为并发设计提供了强大支撑。 Goroutine 调度器 一个 Go 程序对于操作系统来说只是一个用户层程序,操作系统眼中只有线程,它甚至不知道有一种叫 Goroutine 的事物存在。所以,Goroutine 的调度全要靠 Go 自己完成。那么,实现 Go 程序内 Goroutine 之间“公平”竞争“CPU”资源的任务,就落到了Go 运行时(runtime)头上了。要知道在一个 Go 程序中,除了用户层代码,剩下的就是Go 运行时了。 于是,Goroutine 的调度问题就演变为,Go 运行时如何将程序内的众多 Goroutine,按照一定算法调度到“CPU”资源上运行的问题了。 💡 可是,在操作系统层面,线程竞争的“CPU”资源是真实的物理 CPU,但在 Go 程序层 面,各个 Goroutine 要竞争的“CPU”资源又是什么呢? Go 程序是用户层程序,它本身就是整体运行在一个或多个操作系统线程上的。所以这个答案就出来了: Goroutine 们要竞争的“CPU”资源就是操作系统线程。这样,Goroutine调度器的任务也就明确了: 将 Goroutine 按照一定算法放到不同的操作系统线程中去执行。 ...

6 min · 2823 words · Luenci

关于登录认证这件事

聊聊登录认证这件事 这里介绍的是减少登录页面的编写的方法,比如一个公司中有A,B, C 三个系统,对于用户来说希望是我只需要登录(认证)一次就可以访问A,B,C三个系统,而不是进到A系统在A中登录(认证)一次,进到B系统又要在B中登录(认证)一次,进到C系统还要在C中登录(认证)一次,这样一方面会有重复的编码(A,B,C系统的登录页面逻辑),另一方面对用户来说也是非常不友好。 请区别于对系统中的资源的权限校验 关于LDAP 轻量级目录访问协议 (LDAP) 是一种使应用程序可以快速查询用户信息的协议。 “什么是目录服务?” 目录服务是一个特殊的数据库,用来保存描述性的、基于属性的详细信息,支持过滤功能。 是动态的,灵活的,易扩展的。 如:人员组织管理,电话簿,地址簿。 LDAP介绍 LDAP(Light Directory Access Portocol),它是基于 X.500 标准的轻量级目录访问协议。 目录是一个为查询、浏览和搜索而优化的数据库,它成树状结构组织数据,类似文件目录一样。 目录数据库和关系数据库不同,它有优异的读性能,但写性能差,并且没有事务处理、回滚等复杂功能,不适于存储修改频繁的数据。所以目录天生是用来查询的,就好象它的名字一样。 LDAP目录服务是由目录数据库和一套访问协议组成的系统。 LDAP 登录流程 ...

6 min · 2889 words · Luenci