goroutine
在go語言中,每一個并發的執行單元叫做一個goroutine
這裡說到并發,是以先解釋一下并發和并行的概念:
并發:邏輯上具備同時處理多個任務的能力
并行:實體上在同一時刻執行多個并發任務
當一個程式啟動時,其主函數即在一個單獨的goroutine中運作,一般這個goroutine是主goroutine
如果想要建立新的goroutine,隻需要再執行普通函數或者方法的的前面加上關鍵字go
通過下面一個例子示範并發的效果,主goroutine會計算第45個斐波那契函數,在計算的同時會循環列印:-\|/
這裡需要知道:當主goroutine結束之後,所有的goroutine都會被打斷,程式就會退出
package main
import (
"time"
"fmt"
)
func spinner(delay time.Duration){
for {
for _,r := range `-\|/`{
fmt.Printf("\r%c",r)
time.Sleep(delay)
}
}
}
func fib(x int) int{
// 斐波那契函數
if x < 2{
return x
}
return fib(x-1) + fib(x-2)
}
func main() {
go spinner(100*time.Millisecond) //開啟一個goroutine
const n = 45
fibN:= fib(n)
fmt.Printf("\rFib(%d) = %d\n",n,fibN)
}
當第一次看到go的并發,感覺真是太好用了!!!!
是以在網絡程式設計裡,服務端都是需要同時可以處理很多個連接配接,我們看一下下面的服務端和用戶端例子
服務端:
package main
import (
"net"
"io"
"time"
"log"
)
func handleConn(c net.Conn){
defer c.Close()
for{
_,err := io.WriteString(c,time.Now().Format("15:04:05\r\n"))
if err != nil{
return
}
time.Sleep(1*time.Second)
}
}
func main() {
// 監聽本地tcp的8000端口
listener,err := net.Listen("tcp","localhost:8000")
if err != nil{
log.Fatal(err)
}
for {
conn,err := listener.Accept()
if err!= nil{
log.Print(err)
continue
}
go handleConn(conn)
}
}
用戶端:
package main
import (
"io"
"log"
"net"
"os"
)
func mustCopy(dst io.Writer,src io.Reader){
// 從連接配接中讀取内容,并寫到标準輸出
if _,err := io.Copy(dst,src);err !=nil{
log.Fatal(err)
}
}
func main(){
conn,err := net.Dial("tcp","localhost:8000")
if err != nil{
log.Fatal(err)
}
defer conn.Close()
mustCopy(os.Stdout, conn)
}
Channel
channel是不同的goroutine之間的通信機制。
一個goroutine通過channel給另外一個goroutine發送資訊。
每個channel 都有一個特殊的類型,也就是channel可以發送的資料的類型
我們可以通過make建立一個channel如:
ch := make(chan int) 這就是建立了一個類型為int的channel
預設我們這樣建立的是無緩存的channel,當然我們可以通過第二個參數來設定容量
ch := make(chan int,10)
注意:channel是引用類型,channel的零值也是nil
兩個相同類型的channel可以使用==運算符比較。如果兩個channel引用的是相通的對象,那麼比較的結
果為真。一個channel也可以和nil進行比較。
因為channel是在不同的goroutine之間進行通信的,是以channel這裡有兩種操作:存資料和取資料,而這裡兩種操作的
方法都是通過運算符:<-
ch <- x 這裡是發送一個值x到channel中
x = <- ch 這裡是從channel擷取一個值存到變量x
<-ch 這裡是從channel中取出資料,但是不使用結果
close(ch) 用于關閉channel
當我們關閉channel後,再次發送就會導緻panic異常,但是如果之前發送過資料,我們在關閉channel之後依然可以執行接收操作
如果沒有資料的話,會産生一個零值
基于channel發送消息有兩個重要方面,首先每個消息都有一個值,但是有時候通訊的事件和發送的時刻也同樣重要。
我們更希望強調通訊發送的時刻時,我們将它稱為消息事件。有些消息并不攜帶額外的資訊,它僅僅是用做兩個goroutine之間的同步,這個時候我們可以用struct{}空結構體作為channel元素的類型
無緩存的channel
基于無緩存的channel的發送和接受操作将導緻兩個goroutine做一次同步操作,是以無緩存channel有時候也被稱為同步channel
串聯的channel (Pipeline)
channel也可以用于多個goroutine連接配接在一起,一個channel的輸出作為下一個channel的輸入,這種串聯的channel就是所謂的pipeline
通過下面例子了解,第一個goroutine是一個電腦,用于生成0,1,2...形式的整數序列,然後通過channel将該整數序列
發送給第二個goroutine;第二個goroutine是一個求平方的程式,對收到的每個整數求平方,然後将平方後的結果通過第二個channel發送給第三個goroutine
第三個goroutine是一個列印程式,列印收到的每個整數
package main
import (
"fmt"
)
func main(){
naturals := make(chan int)
squares := make(chan int)
go func(){
for x:=0;;x++{
naturals <- x
}
}()
go func(){
for {
x := <- naturals
squares <- x * x
}
}()
for{
fmt.Println(<-squares)
}
}
但是如果我把第一個生成數的寫成一個有範圍的循環,這個時候程式其實會報錯的
是以就需要想辦法讓發送知道沒有可以發給channel的資料了,也讓接受者知道沒有可以接受的資料了
這個時候就需要用到close(chan)
當一個channel被關閉後,再向該channel發送資料就會導緻panic異常
當從一個已經關閉的channel中接受資料,在接收完之前發送的資料後,并不會阻塞,而會立刻傳回零值,是以在從channel裡接受資料的時候可以多擷取一個值如:
go func(){
for {
x ,ok := <-naturals
if !ok{
break
}
squares <- x*x
}
close(squares)
}()
第二位ok是一個布爾值,true表示成功從channel接受到值,false表示channel已經被關閉并且裡面沒有值可以接收
單方向的channel
當一個channel座位一個函數的參數時,它一般總是被專門用于隻發送或者隻接收
chan <- int :表示一個隻發送int的channel,隻能發送不能接收
< chan int : 表示一個隻接受int的channel,隻能接收不能發送
當然在有時候我們需要擷取channel内部緩存的容量,可以通過内置的cap函數擷取
而len函數則傳回的是channel内實際有效的元素個數
基于select的多路複用
這裡先說一個擁有的知識點:time.Tick函數
這個函數傳回一個channel,通過下面代碼進行了解:
package main
import (
"time"
"fmt"
)
func main() {
tick := time.Tick(1*time.Second)
for countdown :=10;countdown>0;countdown--{
j :=<- tick
fmt.Println(j)
}
}
程式會循環列印一個時間戳
select 語句:
select {
case <-ch1:
// ...
case x := <-ch2:
// ...use x...
case ch3 <- y:
// ...
default:
// ...
}
select語句的形式其實和switch語句有點類似,這裡每個case代表一個通信操作
在某個channel上發送或者接收,并且會包含一些語句組成的一個語句塊 。
select中的default來設定當 其它的操作都不能夠馬上被處理時程式需要執行哪些邏輯
channel 的零值是nil, 并且對nil的channel 發送或者接收操作都會永遠阻塞,在select語句中操作nil的channel永遠都不會被select到。
這可以讓我們用nil來激活或者禁用case,來達成處理其他輸出或者輸出時間逾時和取消的邏輯
補充
不同的goroutine之間如何通信
首先我們能夠想到的有:全局變量的方式,我們先通過這種本方法來示範:
package main
import (
"time"
"fmt"
)
var exits [3]bool
func calc(index int){
for i:=0;i<1000;i++{
time.Sleep(time.Millisecond)
}
exits[index] = true
}
func main() {
start := time.Now().UnixNano()
go calc(0)
go calc(1)
go calc(2)
for{
if exits[0] && exits[1] &&exits[2]{
break
}
}
end := time.Now().UnixNano()
fmt.Println("finished,const:%d ms",end-start)
}
這種方法其實比較笨,go為我們提供了鎖同步的方式 sync.WaitGroup,示範代碼為:
//等待一組goroutine執行完成
package main
import (
"time"
"fmt"
"sync"
)
var waitGroup sync.WaitGroup
func calc(index int){
for i:=0;i<1000;i++{
time.Sleep(time.Millisecond)
}
//執行完成的時候Done
waitGroup.Done()
}
func main() {
start := time.Now().UnixNano()
for i:=0;i<3;i++{
// 每次在調用之前add
waitGroup.Add(1)
go calc(i)
}
//在循環外等待wait
waitGroup.Wait()
end := time.Now().UnixNano()
fmt.Println("finished,const:%d ms",end-start)
}
關于Channel的補充
channel 概念:
類似unix中的管道pipe
先進先出
線程安全,多個goroutine同時通路,不需要加鎖
channel是有類型的,一個整數的channel隻能存放整數
定時器的補充
//定時器
package main
import (
"time"
"fmt"
)
func main() {
t := time.NewTicker(time.Second)
for v:= range t.C{
fmt.Println("hello",v)
}
}
// 一次性定時器
package main
import (
"time"
"fmt"
)
func main() {
select{
case <- time.After(time.Second):
fmt.Println("after")
}
}
逾時控制
package main
import (
"time"
"fmt"
)
func queryDb(ch chan int){
time.Sleep(time.Second)
ch <- 100
}
func main() {
ch := make(chan int)
go queryDb(ch)
t := time.NewTicker(time.Second*4)
select{
case v:=<-ch:
fmt.Println("result:",v)
case <-t.C:
fmt.Println("timeout")
關于單元測試和異常捕獲的補充
package main
import (
"time"
"fmt"
)
func calc(){
// defer 定義的後面出現錯誤的都可以捕獲到
defer func() {
err := recover()
if err!=nil{
fmt.Println(err)
}
}()
var p *int
*p = 100
}
關于單元測試的簡單例子示範:
package test
func add(a,b int)int{
return a+b
}
上面的代碼,如果想要做單元測試,我們需要定義一個測試檔案,我把上面代碼的檔案名是calc.go,單元測試代碼的檔案名calc_test.go
package test
import "testing"
func TestAdd(t *testing.T){
result := add(2,3)
if result != 5{
t.Fatalf("add is not right")
return
}
t.Logf("add is right")
}
這樣當我們需要測試add函數的時候隻需要在該包下執行go test,效果如下:
如果想要看更詳細的可以通過go test -v,效果如下:
所有的努力都值得期許,每一份夢想都應該灌溉!