Go对象编程及并发Goroutine机制深入剖析-Coding技术进阶实战
2012 年 4 月 7 日
专注于大数据及容器云核心技术解密,可提供全栈的大数据+云原生平台咨询方案,请持续关注本套博客。QQ邮箱地址:1120746959@qq.com,如有任何学术交流,可随时联系。详情请关注《数据云技术社区》公众号。
1 Go 基本概述
1.1 概述

- 在语言层面实现了并发机制的类C通用型编程语言。
- Go关键字(25个),如上图。
- Go 1.11版本开始支持Go modules方式的依赖包管理功能
hello.go package main import ( "fmt" "rsc.io/quote" ) func main() { fmt.Println(quote.Hello()) } # 安装GO 1.11及以上版本 go version # 开启module功能 export GO111MODULE=on # 进入到项目目录 cd /home/gopath/src/hello # 初始化 go mod init # 编译 go build #加载依赖包,自动归档到vendor目录 go mod vendor -v # 文件目录结构 ./ ├── go.mod ├── go.sum ├── hello # 二进制文件 ├── hello.go └── vendor ├── golang.org ├── modules.txt └── rsc.io 复制代码
- dep安装
go get -u github.com/golang/dep/cmd/dep #进入到项目目录 cd /home/gopath/src/demo #dep初始化,初始化配置文件Gopkg.toml dep init #dep加载依赖包,自动归档到vendor目录 dep ensure # 最终会生成vendor目录,Gopkg.toml和Gopkg.lock的文件 复制代码
2 Go 基本语法
2.1 变量声明
1、单变量声明,类型放在变量名之后,可以为任意类型 var 变量名 类型 2、多变量同类型声明 var v1,v2,v3 string 3、多变量声明 var { v1 int v2 []int } 4、使用关键字var,声明变量类型并赋值 var v1 int=10 5、使用关键字var,直接对变量赋值,go可以自动推导出变量类型 var v2=10 6、直接使用“:=”对变量赋值,不使用var,两者同时使用会语法冲突,推荐使用 v3:=10 7、可以限定常量类型,但非必需 const Pi float64 = 3.14 8、无类型常量和字面常量一样 const zero=0.0 9、多常量赋值 const( size int64=1024 eof=-1 ) 10、常量的多重赋值,类似变量的多重赋值 const u,v float32=0,3 const a,b,c=3,4,"foo" //无类型常量的多重赋值 11、常量赋值是编译期行为,可以赋值为一个编译期运算的常量表达式 const mask=1<<3 复制代码
2.2 变量类型

//布尔类型的关键字为bool,值为true或false,不可写为0或1 var v1 bool v1=true //接受表达式判断赋值,不支持自动或强制类型转换 v2:=(1==2) //int和int32为不同类型,不会自动类型转换需要强制类型转换 //强制类型转换需注意精度损失(浮点数→整数),值溢出(大范围→小范围) var v2 int32 v1:=64 v2=int32(v1) //浮点型分为float32(类似C中的float),float64(类似C中的double) var f1 float32 f1=12 //不加小数点,被推导为整型 f2:=12.0 //加小数点,被推导为float64 f1=float32(f2) //需要执行强制转换 //复数的表示 var v1 complex64 v1=3.2+12i //v1 v2 v3 表示为同一个数 v2:=3.2+12i v3:=complex(3.2,12) //实部与虚部 //z=complex(x,y),通过内置函数实部x=real(z),虚部y=imag(z) //声明与赋值 var str string str="hello world" //创建数组 var array1 [5]int //声明:var 变量名 类型 var array2 [5]int=[5]int{1,2,3,4,5} //初始化 array3:=[5]int{1,2,3,4,5} //直接用“:=”赋值 [3][5]int //二维数组 [3]*float //指针数组 //数组元素访问 for i,v:=range array{ //第一个返回值为数组下标,第二个为元素的值 } //创建切片,基于数组创建 var myArray [5]int=[5]{1,2,3,4,5} var mySlice []int=myArray[first:last] slice1=myArray[:] //基于数组所有元素创建 slice2=myArray[:3] //基于前三个元素创建 slice3=myArray[3:] //基于第3个元素开始后的所有元素创建 //直接创建 slice1:=make([]int,5) //元素初始值为0,初始个数为5 slice2:=make([]int,5,10) //元素初始值为0,初始个数为5,预留个数为10 slice3:=[]int{1,2,3,4,5} //初始化赋值 //基于切片创建 oldSlice:=[]int{1,2,3,4,5} newSlice:=oldSlice[:3] //基于切片创建,不能超过原切片的存储空间(cap函数的值) //动态增减元素,切片分存储空间(cap)和元素个数(len),当存储空间小于实际的元素个数,会重新分配一块原空间2倍的内存块,并将原数据复制到该内存块中,合理的分配存储空间可以以空间换时间,降低系统开销。 //添加元素 newSlice:=append(oldSlice,1,2,3) //直接将元素加进去,若存储空间不够会按上述方式扩容。 newSlice1:=append(oldSlice1,oldSlice2...) //将oldSlice2的元素打散后加到oldSlice1中,三个点不可省略。 //内容复制,copy()函数可以复制切片,如果切片大小不一样,按较小的切片元素个数进行复制 slice1:=[]int{1,2,3,4,5} slice2:=[]int{6,7,8} copy(slice2,slice1) //只会复制slice1的前三个元素到slice2中 copy(slice1,slice1) //只会复制slice2的三个元素到slice1中的前三个位置 //map先声明后创建再赋值 var map1 map[键类型] 值类型 //创建 map1=make(map[键类型] 值类型) map1=make(map[键类型] 值类型 存储空间) //赋值 map1[key]=value // 直接创建 m2 := make(map[string]string) // 然后赋值 m2["a"] = "aa" m2["b"] = "bb" // 初始化 + 赋值一体化 m3 := map[string]string{ "a": "aa", "b": "bb", } //delete()函数删除对应key的键值对,如果key不存在,不会报错;如果value为nil,则会抛出异常(panic)。 delete(map1,key) //元素查找 value,ok:=myMap[key] if ok{ //如果找到 //处理找到的value值 } //遍历 for key,value:=range myMap{ //处理key或value } 复制代码
2.3 流程管理
- 条件语句
//在if之后条件语句之前可以添加变量初始化语句,用;号隔离 if <条件语句> { //条件语句不需要用括号括起来,花括号必须存在 //语句体 }else{ //语句体 } //在有返回值的函数中,不允许将最后的return语句放在if...else...的结构中,否则会编译失败 //例如以下为错误范例 func example(x int) int{ if x==0{ return 5 }else{ return x //最后的return语句放在if-else结构中,所以编译失败 } } 复制代码
- 选择语句
//1、根据条件不同,对应不同的执行体 switch i{ case 0: fmt.Printf("0") case 1: //满足条件就会退出,只有添加fallthrough才会继续执行下一个case语句 fmt.Prinntf("1") case 2,3,1: //单个case可以出现多个选项 fmt.Printf("2,3,1") default: //当都不满足以上条件时,执行default语句 fmt.Printf("Default") } //2、该模式等价于多个if-else的功能 switch { case <条件表达式1>: 语句体1 case <条件表达式2>: 语句体2 } 复制代码
- 循环语句
//1、Go只支持for关键字,不支持while,do-while结构 for i,j:=0,1;i<10;i++{ //支持多个赋值 //语句体 } //2、无限循环 sum:=1 for{ //不接条件表达式表示无限循环 sum++ if sum > 100{ break //满足条件跳出循环 } } //3、支持continue和break,break可以指定中断哪个循环,break JLoop(标签) for j:=0;j<5;j++{ for i:=0;i<10;i++{ if i>5{ break JLoop //终止JLoop标签处的外层循环 } fmt.Println(i) } JLoop: //标签处 ... 复制代码
- 跳转语句
//关键字goto支持跳转 func myfunc(){ i:=0 HERE: //定义标签处 fmt.Println(i) i++ if i<10{ goto HERE //跳转到标签处 } } 复制代码
- 函数定义与调用
//1、函数组成:关键字func ,函数名,参数列表,返回值,函数体,返回语句 //先名称后类型 func 函数名(参数列表)(返回值列表){ //参数列表和返回值列表以变量声明的形式,如果单返回值可以直接加类型 函数体 return //返回语句 } //例子 func Add(a,b int)(ret int,err error){ //函数体 return //return语句 } //2、函数调用 //先导入函数所在的包,直接调用函数 import "mymath" sum,err:=mymath.Add(1,2) //多返回值和错误处理机制 复制代码
- 多返回值
//多返回值 func (file *File) Read(b []byte) (n int,err error) //使用下划线"_"来丢弃返回值 n,_:=f.Read(buf) 复制代码
- 匿名函数
//匿名函数:不带函数名的函数,可以像变量一样被传递。 func(a,b int,z float32) bool{ //没有函数名 return a*b3 对象编程
3.1 对象(属性进行定义,不含方法)
- struct实际上就是一种复合类型,只是对类中的属性进行定义赋值,并没有对方法进行定义,方法可以随时定义绑定到该类的对象上,更具灵活性。可利用嵌套组合来实现类似继承的功能避免代码重复。
type Rect struct{ //定义矩形类 x,y float64 //类型只包含属性,并没有方法 width,height float64 } func (r *Rect) Area() float64{ //为Rect类型绑定Area的方法,*Rect为指针引用可以修改传入参数的值 return r.width*r.height //方法归属于类型,不归属于具体的对象,声明该类型的对象即可调用该类型的方法 } 复制代码3.2 方法(附属到对象)
- 方法:为类型添加方法,方法即为有接收者的函数 func (对象名 对象类型) 函数名(参数列表) (返回值列表), 可随时为某个对象添加方法即为某个方法添加归属对象(receiver)
type Integer int func (a Integer) Less(b Integer) bool{ //表示a这个对象定义了Less这个方法,a可以为任意类型 return a3.3 初始化[实例化对象]
new() func new(Type) *Type 内置函数 new 分配空间。传递给new 函数的是一个类型,不是一个值。返回值是指向这个新分配的零值的指针 //创建实例 rect1:=new(Rect) //new一个对象 rect2:=&Rect{} //为赋值默认值,bool默认值为false,int默认为零值0,string默认为空字符串 rect3:=&Rect{0,0,100,200} //取地址并赋值,按声明的变量顺序依次赋值 rect4:=&Rect{width:100,height:200} //按变量名赋值不按顺序赋值 //构造函数:没有构造参数的概念,通常由全局的创建函数NewXXX来实现构造函数的功能 func NewRect(x,y,width,height float64) *Rect{ return &Rect{x,y,width,height} //利用指针来改变传入参数的值达到类似构造参数的效果 } //方法的重载,Go不支持方法的重载(函数同名,参数不同) //v …interface{}表示参数不定的意思,其中v是slice类型,及声明不定参数,可以传入任意参数,实现类似方法的重载 func (poem *Poem) recite(v ...interface{}) { fmt.Println(v) } 复制代码3.4 匿名组合[继承]
- 组合,即方法代理,例如A包含B,即A通过消息传递的形式代理了B的方法,而不需要重复写B的方法。
func (base *Base) Foo(){...} //Base的Foo()方法 func (base *Base) Bar(){...} //Base的Bar()方法 type Foo struct{ Base //通过组合的方式声明了基类,即继承了基类 ... } func (foo *Foo) Bar(){ foo.Base.Bar() //并改写了基类的方法,该方法实现时先调用基类的Bar()方法 ... //如果没有改写即为继承,调用foo.Foo()和调用foo.Base.Foo()的作用的一样的 } //修改内存布局 type Foo struct{ ... //其他成员信息 Base } //以指针方式组合 type Foo struct{ *Base //以指针方式派生,创建Foo实例时,需要外部提供一个Base类实例的指针 ... } //名字冲突问题,组合内外如果出现名字重复问题,只会访问到最外层,内层会被隐藏,不会报错,即类似java中方法覆盖/重写。 type X struct{ Name string } type Y struct{ X //Y.X.Name会被隐藏,内层会被隐藏 Name string //只会访问到Y.Name,只会调用外层属性 } 复制代码3.5 可见性[封装]
- 封装的本质或目的其实程序对信息(数据)的控制力。封装分为两部分:该隐藏的隐藏,该暴露的暴露。封装可以隐藏实现细节,使得代码模块化。
type Rect struct{ X,Y float64 Width,Height float64 //字母大写开头表示该属性可以由包外访问到 } func (r *Rect) area() float64{ //字母小写开头表示该方法只能包内调用 return r.Width*r.Height } 复制代码3.6 接口[多态]
- Go语言的接口是隐式存在,只要实现了该接口的所有函数则代表已经实现了该接口,并不需要显式的接口声明。
- Go语言非侵入式接口:一个类只需要实现了接口要求的所有函数就表示实现了该接口,并不需要显式声明
type File struct{ //类的属性 } //File类的方法 func (f *File) Read(buf []byte) (n int,err error) func (f *File) Write(buf []byte) (n int,err error) func (f *File) Seek(off int64,whence int) (pos int64,err error) func (f *File) Close() error //接口1:IFile type IFile interface{ Read(buf []byte) (n int,err error) Write(buf []byte) (n int,err error) Seek(off int64,whence int) (pos int64,err error) Close() error } //接口2:IReader type IReader interface{ Read(buf []byte) (n int,err error) } //接口赋值,File类实现了IFile和IReader接口,即接口所包含的所有方法 var file1 IFile = new(File) var file2 IReader = new(File) 复制代码
- 只要类实现了该接口的所有方法,即可将该类赋值给这个接口,接口主要用于多态化方法。即对接口定义的方法,不同的实现方式。
//接口animal type Animal interface { Speak() string } //Dog类实现animal接口 type Dog struct { } func (d Dog) Speak() string { return "Woof!" } //Cat类实现animal接口 type Cat struct { } func (c Cat) Speak() string { return "Meow!" } //Llama实现animal接口 type Llama struct { } func (l Llama) Speak() string { return "?????" } //JavaProgrammer实现animal接口 type JavaProgrammer struct { } func (j JavaProgrammer) Speak() string { return "Design patterns!" } //主函数 func main() { animals := []Animal{Dog{}, Cat{}, Llama{}, JavaProgrammer{}} //利用接口实现多态 for _, animal := range animals { fmt.Println(animal.Speak()) //打印不同实现该接口的类的方法返回值 } } 复制代码4 Goroutine机制
- 执行体是个抽象的概念,在操作系统中分为三个级别:进程(process),进程内的线程(thread),进程内的协程(coroutine,轻量级线程)。
- 协程的数量级可达到上百万个,进程和线程的数量级最多不超过一万个。
- Go语言中的协程叫goroutine,Go标准库提供的调用操作,IO操作都会出让CPU给其他goroutine,让协程间的切换管理不依赖系统的线程和进程,不依赖CPU的核心数量。
- 并发编程的难度在于协调,协调需要通过通信,并发通信模型分为共享数据和消息.
- 共享数据即多个并发单元保持对同一个数据的引用,数据可以是内存数据块,磁盘文件,网络数据等。数据共享通过加锁的方式来避免死锁和资源竞争.
- Go语言则采取消息机制来通信,每个并发单元是独立的个体,有独立的变量,不同并发单元间这些变量不共享,每个并发单元的输入输出只通过消息的方式。
//定义调用体 func Add(x,y int){ z:=x+y fmt.Println(z) } //go关键字执行调用,即会产生一个goroutine并发执行 //当函数返回时,goroutine自动结束,如果有返回值,返回值会自动被丢弃 go Add(1,1) //并发执行 func main(){ for i:=0;i<10;i++{//主函数启动了10个goroutine,然后返回,程序退出,并不会等待其他goroutine结束 go Add(i,i) //所以需要通过channel通信来保证其他goroutine可以顺利执行 } } 复制代码
- channel就像管道的形式,是goroutine之间的通信方式,是进程内的通信方式,跨进程通信建议用分布式系统的方法来解决,例如Socket或http等通信协议。channel是类型相关,即一个channel只能传递一种类型的值,在声明时指定。
//1、channel声明,声明一个管道chanName,该管道可以传递的类型是ElementType //管道是一种复合类型,[chan ElementType],表示可以传递ElementType类型的管道[类似定语从句的修饰方法] var chanName chan ElementType var ch chan int //声明一个可以传递int类型的管道 var m map[string] chan bool //声明一个map,值的类型为可以传递bool类型的管道 复制代码
- 缓冲机制:为管道指定空间长度,达到类似消息队列的效果
//缓冲机制 c:=make(chan int,1024) //第二个参数为缓冲区大小,与切片的空间大小类似 //通过range关键字来实现依次读取管道的数据,与数组或切片的range使用方法类似 for i :=range c{ fmt.Println("Received:",i) } //超时机制:利用select只要一个case满足,程序就继续执行而不考虑其他case的情况的特性实现超时机制 timeout:=make(chan bool,1) //设置一个超时管道 go func(){ time.Sleep(1e9) //设置超时时间,等待一分钟 timeout<-true //一分钟后往管道放一个true的值 }() // select { case <-ch: //如果读到数据,则会结束select过程 //从ch中读取数据 case <-timeout: //如果前面的case没有调用到,必定会读到true值,结束select,避免永久等待 //一直没有从ch中读取到数据,但从timeout中读取到了数据 } 复制代码
- 管道读写
//管道写入,把值想象成一个球,"<-"的方向,表示球的流向,ch即为管道 //写入时,当管道已满(管道有缓冲长度)则会导致程序堵塞,直到有goroutine从中读取出值 ch <- value //管道读取,"<-"表示从管道把球倒出来赋值给一个变量 //当管道为空,读取数据会导致程序阻塞,直到有goroutine写入值 value:= <-ch 复制代码
- select机制
//每个case必须是一个IO操作,面向channel的操作,只执行其中的一个case操作,一旦满足则结束select过程 //面向channel的操作无非三种情况:成功读出;成功写入;即没有读出也没有写入 select{ case <-chan1: //如果chan1读到数据,则进行该case处理语句 case chan2<-1: //如果成功向chan2写入数据,则进入该case处理语句 default: //如果上面都没有成功,则进入default处理流程 } 复制代码5 Goroutine调度
- M:machine,代表系统内核进程,用来执行G。(工人)
- P:processor,代表调度执行的上下文(context),维护了一个本地的goroutine的队列。(小推车)
- G:goroutine,代表goroutine,即执行的goroutine的数据结构及栈等。(砖头)
5.1 调度本质
- 调度的本质是将G尽量均匀合理地安排给M来执行,其中P的作用就是来实现合理安排逻辑。
5.2 抢占式调度(阻塞)
- 当goroutine发生阻塞的时候,可以通过P将剩余的G切换给新的M来执行,而不会导致剩余的G无法执行,如果没有M则创建M来匹配P。
5.3 偷任务
P可以偷任务(即goroutine),当某个P的本地G执行完,且全局没有G需要执行的时候,P可以去偷别的P还没有执行完的一半的G来给M执行,提高了G的执行效率。
6 总结
专注于大数据及容器云核心技术解密,可提供全栈的大数据+云原生平台咨询方案,请持续关注本套博客。QQ邮箱地址:1120746959@qq.com,如有任何学术交流,可随时联系。详情请关注《数据云技术社区》公众号。