Go對象編程及併發Goroutine機制深刻剖析-Coding技術進階實戰

專一於大數據及容器雲核心技術解密,可提供全棧的大數據+雲原平生臺諮詢方案,請持續關注本套博客。QQ郵箱地址:1120746959@qq.com,若有任何學術交流,可隨時聯繫。詳情請關注《數據雲技術社區》公衆號。 java

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 變量聲明

一、單變量聲明,類型放在變量名以後,能夠爲任意類型
var 變量名 類型

二、多變量同類型聲明
var v1,v2,v3 string 

三、多變量聲明
var {
    v1 int
    v2 []int
}

四、使用關鍵字var,聲明變量類型並賦值
var v1 int=10

五、使用關鍵字var,直接對變量賦值,go能夠自動推導出變量類型
var v2=10

六、直接使用「:=」對變量賦值,不使用var,二者同時使用會語法衝突,推薦使用
v3:=10

七、能夠限定常量類型,但非必需
const Pi float64 = 3.14

八、無類型常量和字面常量同樣
const zero=0.0

九、多常量賦值
const(
  size int64=1024
  eof=-1
)

十、常量的多重賦值,相似變量的多重賦值
const u,v float32=0,3
const a,b,c=3,4,"foo"    //無類型常量的多重賦值

十一、常量賦值是編譯期行爲,能夠賦值爲一個編譯期運算的常量表達式
const mask=1<<3

複製代碼

2.2 變量類型

//布爾類型的關鍵字爲bool,值爲truefalse,不可寫爲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結構中,因此編譯失敗
  }
}
複製代碼
  • 選擇語句
//一、根據條件不一樣,對應不一樣的執行體
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")
}

//二、該模式等價於多個if-else的功能
switch {
  case <條件表達式1>:
      語句體1
  case <條件表達式2>:
      語句體2
}
複製代碼
  • 循環語句
//一、Go只支持for關鍵字,不支持whiledo-while結構
for i,j:=0,1;i<10;i++{    //支持多個賦值
  //語句體
}

//二、無限循環
sum:=1
for{  //不接條件表達式表示無限循環
  sum++
  if sum > 100{
    break   //知足條件跳出循環
  }
}

//三、支持continuebreakbreak能夠指定中斷哪一個循環,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     //跳轉到標籤處
  }
}
複製代碼
  • 函數定義與調用
//一、函數組成:關鍵字func ,函數名,參數列表,返回值,函數體,返回語句
//先名稱後類型
func 函數名(參數列表)(返回值列表){  //參數列表和返回值列表以變量聲明的形式,若是單返回值能夠直接加類型
  函數體
  return    //返回語句
}
//例子
func Add(a,b int)(ret int,err error){
  //函數體 
  return   //return語句
}

//二、函數調用
//先導入函數所在的包,直接調用函數
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只能傳遞一種類型的值,在聲明時指定。
//一、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的執行效率。git

6 總結

專一於大數據及容器雲核心技術解密,可提供全棧的大數據+雲原平生臺諮詢方案,請持續關注本套博客。QQ郵箱地址:1120746959@qq.com,若有任何學術交流,可隨時聯繫。詳情請關注《數據雲技術社區》公衆號。github

相關文章
相關標籤/搜索