Go Type System(二)

函数

  • 什么是Function Signatures? 什么是Function Types? 什么是Function Type Literals? 什么是Function Prototypes? 什么是Function Declaration?以func Double(n int) (result int)为例,进行解释。

    • 函数签名 = 输入参数类型的列表 + 输出结果类型的列表。例如,(n int) (result int)
    • 函数类型 = func + 函数签名。例如,func (n int) (result int)。根据Type Literal的规定,func (n int) (result int)也被称为Function Type Literals,属于Undefined Types。函数类型声明的方式type Myfunc func (n int) (result int)
    • 函数原型 = 函数名称 + 函数类型。例如,func Double(n int) (result int)就是一个函数原型。函数类型要采用Literal的形式。
    • 函数声明(Function Declaration,不是Function Type Declaration) = 函数原型 + 函数体。
    • 以上是函数作为一等公民的必要解释。
  • Function Value具有哪些特殊性?

    • Function Value除了可以被传递,还可以被调用。由于Built-in Function采用了“泛型”作为参数,它不是Function Value;由于init函数的特殊性,它也不能作为Function Value
    • 当我们声明了一个自定义的函数,我们也声明可一个Function Value,它的标识就是函数名称,它不可被修改
    • 如果函数值为nil,调用时发生panic;如果是go + 函数值调用,会发生不可恢复的panic。
    • ** All functions in Go can be viewed as closures. **

Goroutine

Goroutine是Go语言提供的唯一的并发编程(concurrent Programming)手段。Go不提供创建线程的手段。当程序启动时,它只有一个协程,称为主协程(main goroutine)。你可以很方便地通过go + 函数调用,创建一个新的协程。新协程退出的时机是函数调用返回。注意,如果主协程退出,那么所有其它的协程都会退出,整个进程退出。

Goroutine有四种状态:createrunningblockingexitblocking状态是指协程将会处于blocking状态,直到由其它协程唤醒。对于调用time.Sleep函数,导致协程暂停执行的情况,我们认为协程仍然处于running状态(因为无需其它协程来唤醒)。在状态转换方向中,create只能转换为running;running能转换为blocking或exit状态;blocking只能转换为running状态

协程的并行度的上限是逻辑CPU的个数(Logical CPU),调用runtime.NumCPU,获取Go程序可使用的逻辑CPU的个数。

为什么不是操作系统的逻辑CPU的个数?某些逻辑CPU可能被其它进程锁定。

那么,是否意味着Go程序会启动runtime.NumCPU个线程,跑满所有的CPU?如何控制CPU的使用量?Go支持创建成千上万个协程,多个协程对应一个线程,如果协程调用系统调用,阻塞线程,runtime.NumCPU个线程岂不是会都处于阻塞状态?带着这些问题,我们来学习Go的调度模型。

为了提升并发的性能,要做到:降低上下文切换的代价;充分利用操作系统的多个CPU。线程上下文的切换代价越来越高,如果一个协程对应一个线程,一旦创建成千上万个线程,上下文切换将成为系统的沉重负担。一个协程对应一个线程,也会造成垃圾收集器的难于实现(只有当memory must be in a consistent state时,Go才能进行垃圾收集工作,为此,我们必须wait for the threads that are being actively run on a CPU core)。于是,Go实现了用户态的调度器。

Go采用了MPG模型。M代表系统线程,P代表上下文(Processor),G代表协程(Goroutine)。每个Processor都维护了一个runqueue队列,队列中的每个元素都是一个都处于running状态的协程。新创建的协程会被某个Processor接收,并被放置到队尾。系统线程若想执行协程,它必须Hold住一个Processor,从队列中摘取一个协程,运行之。

如果某个协程因为执行系统调用,会不会导致对应的线程被阻塞?如果多个协程执行系统调用,会不会导致所有线程都处于阻塞状态?原先的线程会被阻塞,但Go调度器会创建一个新的线程,并将Processor转交给新的线程,继续执行队列中的其它协程。所以,P个Processors总是处于"active"状态。被阻塞的线程恢复执行后,它要么从其它线程那里偷取一个Processor,继续执行;要么将Go协程放入全局runqueue,自己返回到线程池当中。

如何控制并发度?调用runtime.GOMAXPROCS函数或者设置GOMAXPROCS环境变量。For the standard Go runtime, before Go 1.5, the default initial value of this number is 1, but since Go 1.5, the default initial value of this number is equal to the number of logical CPUs available for the current running program.

M与P的数量关系?默认情况下,M等于P。你通过runtime.GOMAXPROCS可以设置P为更大的值;P在超过逻辑CPU个数之后,更大的P值没什么意义。此时,一个M可能会对应多个P。

如何保证负载均衡?如果某些Processor的队列已空,对应的线程将没有工作可做。调度器会从其它Processor的队列中,偷取Go协程,确保所有线程的负载均衡。

Concurrency Synchronization

当一块内存同时被读写时(多一个协程读、一个协程写;多个协程同时写),会破坏数据的完整性。这种情况被称为data race

协程也是一种资源,处于blocking状态的协程,如果再也没有机会被唤醒,那就存在资源泄露。所以,当你创建协程时,需要考虑以下情况:1. 需要创建多少个协程;2. 这些协程何时启动、阻塞、解除阻塞、退出;3. 如何为这些协程分配任务。

concurrency synchronizationdata synchronization就是负责解决这些问题。Go提供了三种进程内同步手段:1. channel;2. sync package;3. sync/atomic。channel和goroutine大大降低了并发编程的难度,Gopher很乐意使用它们,而忽略了传统的data sync技术(包括sync和sync/atomic)。

Channels

  • Channel的可比较的(comparable)?

    • 综合Channel的内部表示type _channel *channelImpl,两个channel相等的条件是:_channel的值相等。
    • 不考虑channel内部的element。
  • Channel的五大操作?

    • close(ch)
    • <- ch
    • ch <- v
    • len(ch)
    • cap(ch)
    • 以上五大操作都是同步操作(synchronized operations)。但Go语言中的绝大部分操作都不是同步操作,甚至包括:v = <- ch的赋值给v的部分;ch1 := ch2
Operation A Nil Channel A Closed Channel A Not-Closed Non-Nil Channel
Close panic panic succeed to close
Send Value To block for ever panic block or succeed to send
Receive Value From block for ever never block block or succeed to receive
  • Channel内部的三个队列

    • the receiving goroutine queue 这是一个链表结构的队列,没有长度限制。在这个队列中的协程都处于阻塞状态(blocking state),等待从该Channel中获取数据;
    • the sending goroutine queue 这也是一个链表结构的队列,没有长度限制。在这个队列中的协程都处于阻塞状态(blocking state),等待向该Channel发送数据。 等待被发送的数据也存储在这个队列中。
    • the value buffer queue 这是一个环形队列,它的大小等于Channel的容量(capacity). 队列中存储的是Channel的元素。 该队列有两个特殊状态:full status(环形队列装满了值) 和 empty status(环形队列为空). 对于unbuffered channel,队列既处于full status也处于empty status。
    • 另外,这个Channel还拥有一个互斥锁(mutex lock)
  • 当协程从一个"not-nil not-closed channel"中获取数据

    • 如果value buffer queue不为空(此时receiving goroutine queue一定为空),协程A将会从value buffer queue中摘下一个元素(A接收到的元素)。 如果sending goroutine queue此时也不为空,一个发送协程将会被摘下(同时,协程进入running状态);发送的数据将会被放入value buffer queue。 协程A一直处于running状态(non-blocking operation)。
    • 如果value buffer queue为空。如果sending goroutine queue不为空,说明存在阻塞的协程(说明Channel本身是一个unbuffered channel) 一个发送协程将会被摘下, 进入running状态, 发送的数据直接返回给协程A。 协程A一直处于running状态(non-blocking operation)。
    • 如果value buffer queue为空且sending goroutine queue为空, 协程A将会被挂载到receiving goroutine queue,并进入阻塞(blocking)状态.
  • 当协程向一个"not-nil not-closed channel"中发送数据

  • 如果receiving goroutine queue不为空,此时value buffer queue一定为空(如果value buffer queue不为空,receiving goroutine queue就应该为空才对)。 一个接收协程将会被摘下(进入running状态),并将数据发送给该协程。

  • 如果receiving goroutine queue为空,此时如果value buffer queue还有存储空间(没有达到full状态),此时sending goroutine queue一定为空。协程A将数据放入value buffer queue

  • 如果receiving goroutine queue为空,此时如果value buffer queue已经处于full状态,协程A将会被挂载到sending goroutine queue, 进入阻塞状态。

  • 当协程关闭一个"not-nil not-closed channel"

    • 如果receiving goroutine queue不为空(此时value buffer queue一定为空), 所有被挂载的协程将会被摘下,并设置为running状态,每个协程返回了零值。
    • 如果sending goroutine queue不为空, 每一个被挂载的发送协程将会被摘下,每个协程将会产生一个panic(for sending on a closed channel)。 注意:value buffered queue中的数据仍然存在,仍然可以被正常消费。
  • 当协程从一个"not-nil closed channel"中获取数据

    • 如果value buffered queue中存在数据,则从队列中取出第一个元素,并返回。
    • 如果value buffered queue中没有数据,则返回零值。
    • 接收操作的第二个参数表示是否还有数据要继续读,If the second return result is false, then the first return result (the received value) must be a zero value of the element type of the channel.
  • 两个协程之间通过channel(communication)进行数据拷贝

    • 如果channel是unbuffered类型,则数据拷贝一次;
    • 如果channel是buffered类型,数据拷贝两次。
  • for-range On Channels

    • 直到管道关闭且value buffer queue为空(until the channel is closed and its value buffer queue becomes blank.)
    • 注意,如果aChannel等于nil,则进入死循环。相当于receive from a nil channel。
  • select的工作原理

      1. 求值。evaluate all involved channel expressions and value expressions to be potentially sent in case operations, from top to bottom and left to right. Destination values for receive operations (as source values) in assignments needn’t to be evaluated at this time.
      1. 随机化case分支。The default branch is always put at the last position in the result order. Channels may be duplicate in the case operations.
      1. 对所有Channel进行排序(与第2步的结果没关系)。sort all involved channels in the case operations to avoid deadlock in the next step. No duplicate channels stay in the first N channels of the sorted result, where N is the number of involved channels in the case operations. Below, the channel lock order is a concept for the first N channels in the sorted result.
      1. 加锁。lock (a.k.a., acquire the locks of) all involved channels by the channel lock order produced in last step.
      1. Poll。(poll each branch in the select block by the randomized order produced in step 2)
      • if this is a case branch and the corresponding channel operation is a send-value-to-closed-channel operation, 解锁并panic. Go to step 12.
      • if this is a case branch and the corresponding channel operation is non-blocking, 执行channel读写操作,解锁,执行分支代码. Go to step 12.
      • if this is the default branch, 解锁并执行default之分. Go to step 12.
      1. 从这一步开始将处理block operation的步骤。将协程放入receiving goroutine queue 或 sending goroutine queue. 因为多个case分支可以对同一个channel操作,故channel的两个队列可以同时拥有对这个协程的引用。每一个队列可能会有多个对这个协程的引用。
      1. 当前协程进入休眠状态,并释放所有的锁
      1. waiting other channel operations to wake up the current goroutine
      1. 如果其它协程(B)是a channel send/receive 操作,那么B会从goroutine queue中摘下一个协程(select的那个协程),select协程还记录了自己所在的case分支。
      1. 加锁
      1. 对select所涉及到的channels进行检查,并从它们的goroutine queue中再一次摘除下来(毕竟,select协程已经处于running状态了) dequeue the current goroutine from the receiving goroutine queue or sending goroutine queue of the involved channel in each case operation.
      • if the current goroutine is waken up by a channel close operation, go to step 5. 其它协程的close操作并不能对应到具体的case语句,重新进行Poll.
      • if the current goroutine is waken up by a channel send/receive operation, unlock并执行case语句
      1. Done.

method

  • Method Declaration的要求?

    • T 必须是Defined Types
    • T 不能是Pointer TypeInterface Type
    • T 和 它的方法必须是在同一个package中。
  • 编译器的额外作用?

    • 编译器为每个方法生成对应的函数。
      • 如果你创建了方法func (b Book) Pages() int,那么你可以调用Book.Pages函数。
      • 如果你创建了func (b *Book) SetPages(pages int)方法,你可以调用(*Book).SetPages(&book, 123)
      • 编译器还会重写方法,让方法调用生成的函数。
    • 对于value receiver type的方法A。
      • 编译器为value receiver type的方法生成对应的函数X。
      • 编译器为pointer receiver type生成对应的方法B。
      • 编译器为pointer receiver type的方法,生成对应的函数Y。
      • A,B,Y的内部只有一行代码,调用函数X。
  • Method PrototypesMethod Sets

    • 方法原型与函数原型类似,只是缺少了关键字func,例如,SetPages(pages int)
    • 一个类型的所有方法构成了Method Sets所有类型都有一个Method Set,即使是空。
    • 一个Interface类型可以等价于一个Method Set。即,Interface的类型信息并没有那么地重要。比如,如果两个Defined InterfaceMethod Set相同,那么它们就可以相互赋值。
  • Method ValuesMethod Calls

    • s.m 称为Method Values.m()称为Method Call
    • s.m的类型信息是Function Type,内容不包括receiver部分。也可以作为Function Value
  • 方法的receiver类型应该是 value 还是 pointer?

    • If the size of a value receiver type is large, then the receiver argument copy cost may be not negligible. Pointer types are all small-size types.
    • Values of the types in the sync standard package should not be copied, so defining methods with value receivers for struct types which embedding the types in the sync standard package is problematic.
    • Declaring methods of both value receivers and pointer receivers for the same base type is more likely to cause data races if the declared methods are called concurrently in multiple goroutines.
    • Too many pointer copies may cause heavier workload for garbage collector.
    • If it is hard to make a decision whether a method should use a pointer receiver or a value receiver, then just choose the pointer receiver way.

Interface

  • Interface的内部结构

    • Blank Interface Type
      type _interface struct {
        dynamicType  *_type         // the dynamic type
        dynamicValue unsafe.Pointer // the dynamic value
      }
      
    • non-Blank Interface Type
      type _interface struct {
        dynamicTypeInfo *struct {
            dynamicType *_type       // the dynamic type
            methods     []*_function // method table
        }
        dynamicValue unsafe.Pointer // the dynamic value
      }
      
  • 什么是 Interface Types?

    • Interface Type基本上等价于它的Method Set。如果两个不同的Defined Interface Type,它们的Method Set一致,那么它们就可以相互赋值。这引入了另外一个概念Implementation
    • 所有的类型都拥有一个Method Set,例如,int类型的Method Set为空,它实现了interface{}接口。
  • Implementations的定义?

    • If the method set of an arbitrary type T, T may be an interface type or not, is a super set of the method set of an interface type I, then we say type T implements interface I.
    • In Go, if a type T implements an interface type I, then any value of type T can be implicitly converted to type I.
  • 什么是 Value Boxing? 赋值的过程都发生了什么?

    • 如果类型T实现了接口I,i = t操作才合法,才会发生Boxing
    • if type T is a non-interface type, then a copy of the T value is boxed (or encapsulated) into the result (or destination) I value. 这是O(N)操作。
    • if type T is also an interface type, then a copy of the value boxed in the T value is boxed (or encapsulated) into the result (or destination) I value. 通过编译器优化,这是O(1)操作。
  • i = nil VS i = []int(nil)的区别?

    • i = nil 将变量i设置为Zero Value,清除它的Dynamic ValueDynamic Type
    • i = []int(nil) 设置i的值,它的Dynamic Type[]int类型,它的Dynamic Value是nil。所以i不是Zero Value。
  • Type Assertion的四种情况

    • convert a non-interface value to an interface value, where the type of the non-interface value must implement the type of the interface value.
    • convert an interface value to an interface value, where the type of the source interface value must implement the type of the destination interface value.
    • convert an interface value to a non-interface value, where the type of the non-interface value must implement the type of the interface value.
    • convert an interface value to an interface value, where the type of the source interface value may or may not implement the type of the destination interface value.
      针对后两种情况,需要执行t := i.(T) 或 t, ok := i.(T)操作,称为Type Assertion
  • Interface values involved comparisons

    • 如果一个是interface类型,一个是非interface类型,则非interface类型需要实现了interface类型,将非interface类型转换为interface类型。再比较。
    • 两个不同类型的interface之间是可以比较的!!
    • if one of the two interface values is a nil interface value, then the comparison result is whether or not the other interface value is also a nil interface value.
    • if the dynamic types of the two interface values are two different types, then the comparison result is false
    • for the case of the dynamic types of the two interface values are the same type
      • if the same dynamic type is an incomparable type, a panic will occur.
      • otherwise, the comparison result is the result of comparing the dynamic values of the two interface values.

Type Embedding

  • Embedded Type的要求是什么?

    • 嵌套的两种形式T or *T
    • T必须是拥有自己的Identifier
    • T 不能是Defined Pointer Type 或者 a pointer type whose base type is a pointer or interface type
  • Type Embedding 的本质是什么?

    • 每个 Anonymous Field 都有名字,与Type Name同名。这要求Embedded Type不一定是Defined Type,但一定能denoted by a name,特指Type Alias Definition
    • *intint不能同时作为Embedded Type,否则出现了同名的冲突。
    • c.x其实是a Shorthands of Selectors。它可以表示为:c.A.xc.B.xc.B.A.x。其中,最后一种形式成为full form。所以,the depth of the selector c.x is 2。根据selector depth我们定义了Selector ShadowingSelector Colliding的规则。
  • 假设S是Embedding Type,T是Embedded Type,解释编译器为S生成的隐式方法(Implicit Methods)

    • 针对T的方法m,如果它既没有与其它方法发生冲突(Selector Colliding),也没有发生Selector Shadowing,编译器为S生成相同的方法原型。同时,编译器也会为*S生成相同的方法原型
    • 针对*T的方法m,如果它既没有与其它方法发生冲突(Selector Colliding),也没有发生Selector Shadowing,编译器只会为*S生成相同的方法声明
    • 所以,type struct{T} and type *struct{T} both obtain all the methods of the type denoted by T.
    • 所以,type struct{T}, type struct{T}, and type struct{T} obtains all the methods of type *T.
    • 所以,S即使是Undefined Type,你虽不能定义自己的方法,但是通过Type Embedding技术,编译器会为你生成一些方法。
  • Type Embedding例子

    type MyInt int
    func (mi MyInt) IsOdd() bool {
      return mi%2 == 1
    }
    type Age MyInt  // Age不包含任何方法,因为不是`Embed`
    type X struct {  // X 有Double和IsOdd两个方法
      MyInt
    }
    func (x X) Double() MyInt {
      return x.MyInt + x.MyInt
    }
    type Y struct {  // Y 不含有任何方法
      Age
    }
    type Z X  // Z 只包含IsOdd方法
    
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容

  • rljs by sennchi Timeline of History Part One The Cognitiv...
    sennchi阅读 7,279评论 0 10
  • 标签(空格分隔): 编程 Go官方文档 Using the tour 1.1 Hello, 世界 Welcome...
    uangianlap阅读 1,488评论 0 5
  • 通过egret launcher 找到使用引擎版本的本地文件夹位置 修改src中对应文件的代码 回到项目中输入下面...
    Zszen阅读 1,681评论 0 51
  • 这是Spenser的第498篇文章 2月14日对于大部分人来说,是吃狗粮的情人节,对于京东来说,是大老板刘强东的生...
    Spenser陈立飞阅读 278评论 0 1