Go对象编程及并发Goroutine机制深入剖析-Coding技术进阶实战

专注于大数据及容器云核心技术解密,可提供全栈的大数据+云原生平台咨询方案,请持续关注本套博客。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*b

3 对象编程

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 a

3.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,如有任何学术交流,可随时联系。详情请关注《数据云技术社区》公众号。