42

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

 4 years ago
source link: https://www.tuicool.com/articles/mqyme2U
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

专注于大数据及容器云核心技术解密,可提供全栈的大数据+云原生平台咨询方案,请持续关注本套博客。QQ邮箱地址:[email protected],如有任何学术交流,可随时联系。详情请关注《数据云技术社区》公众号。

1 Go 基本概述

1.1 概述

fQjua2E.png!web
  • 在语言层面实现了并发机制的类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 变量类型

RZJNram.png!web
//布尔类型的关键字为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<int(z)
}
f:=func(x,y int) int{
  return x+y
}
复制代码

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<b                           
}
//类型基于值传递,如果要修改值需要传递指针
func (a *Integer) Add(b Integer){
  *a+=b    //通过指针传递来改变值
}
复制代码

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邮箱地址:[email protected],如有任何学术交流,可随时联系。详情请关注《数据云技术社区》公众号。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK