雲原生時代崛起的程式語言Go並行程式設計實戰

2023-05-05 06:00:42

@

概述

基礎理論

Do not communicate by sharing memory; instead, share memory by communicating

也即是不要通過共用記憶體來通訊,相反的要通過通訊來實現記憶體共用;使用通道來控制存取可以更容易地編寫清晰、正確的程式。

簡單來說所謂並行程式設計是指在一個處理器上「同時」處理多個任務;宏觀上並行是指在一段時間內,有多個程式在同時執行;在微觀上 並行是指在同一時刻只能有一條指令執行,但多個程式指令被快速的輪換執行,使得在宏觀上具有多個程序同時執行的效果,但在微觀上並不是同時執行的,只是把時間分成若干段,使多個程式快速交替的執行。

在許多環境中,實現對共用變數的正確存取使得並行程式設計變得困難。Go鼓勵通過共用值在通道上傳遞,實際上沒有被單獨的執行執行緒主動共用。在任何給定時刻只有一個執行緒可以存取該值,因此在資料競爭在設計上是不會發生的。單執行緒程式不需要同步原語,也不需要同步。如果通訊是同步器,則仍然不需要其他同步。例如,Unix管道就非常適合這個模型;儘管Go的並行方法起源於Hoare的交談循序程式(CSP),但也可以被視為Unix管道的型別安全泛化。

並行原語

在作業系統中,往往設計一些完成特定功能的、不可中斷的過程,這些不可中斷的過程稱為原語。並行原語就是在程式語言設計之初以及後續的擴充套件過程中,專門為並行設計而開發的關鍵詞或程式碼片段或一部分功能,進而能夠為該語言實現並行提供更好的支援。

  • Go官方提供並行原語:goroutine、sync包下的Mutex、RWMutex、Once、WaitGroup、Cond、channel、Pool、Context、Timer、atomic等等。
  • 擴充套件並行原語:Semaphore、SingleFlight、CyclicBarrier、ReentrantLock等等。

協程-Goroutine

在Go語言中,每一個並行的執行單元叫作一個goroutine,它是一個輕量級的執行執行緒,被稱為協程,有別於執行緒、程序程等。協程以簡單的模型執行,在同一地址空間中與其他執行協程並行執行的函數;只需要分配堆疊空間。堆疊開始時很小因此開銷很低,並按需分配實現堆空間申請和釋放。執行緒被多路複用到多個作業系統執行緒上,所以如果一個執行緒阻塞了,比如在等待I/O時,其他執行緒會繼續執行。Goroutines設計隱藏了執行緒建立和管理的許多複雜性。在Go語言開啟協程非常簡單,在函數或方法呼叫前加上go關鍵字,例如有一個函數呼叫f(s),這種呼叫它的方式是同步,而在程式中使用go f(s)呼叫,則會新開協程將與呼叫協程並行執行。

package main

import (
    "fmt"
    "time"
)

func f(from string) {
    for i := 0; i < 3; i++ {
        fmt.Println(from, ":", i)
    }
}

func main() {

    f("direct")

    go f("goroutine")

    go func(msg string) {
        fmt.Println(msg)
    }("going")

    time.Sleep(time.Second)
    fmt.Println("done")
}

通道-Channel

Channels是一種程式設計結構,允許在程式碼的不同部分之間行動資料,通常來自不同的 goroutine。與對映一樣,Channels通道也使用make分配,返回對底層資料結構的參照。如果提供了一個可選的整數引數則可設定通道的緩衝區大小。對於非緩衝通道或同步通道,預設值為零。無緩衝通道將通訊(值的交換)與同步結合起來,保證兩個計算(例程)處於已知狀態。

通道是連線並行程式的管道,可以從一個執行協程向通道傳送值,並從另一個執行協程接收這些值。預設情況下,通道是無緩衝的,這意味著只有當有相應的接收(<- chan)準備接收傳送的值時,通道才會接受傳送(chan <-)。緩衝通道接受有限數量的值,而沒有相應的接收器接收這些值。還可以使用通道來同步跨程式的執行,使用阻塞接收來等待程式完成,而需要等待多個協程完成時可能更多會使用WaitGroup,後面再介紹;當使用通道作為函數引數時,可以指定通道是隻傳送還是接收值,也叫做定向通道,其增加了程式的型別安全性。

package main

import (
	"fmt"
	"time"
)

func worker(done chan bool) {
	fmt.Print("working...")
	time.Sleep(time.Second)
	fmt.Println("done")

	done <- true
}

func ping(pings chan<- string, msg string) {
	pings <- msg
}

func pong(pings <-chan string, pongs chan<- string) {
	msg := <-pings
	pongs <- msg
}

func main() {

	messages := make(chan string)

	go func() { messages <- "ping" }()

	msg := <-messages
	fmt.Println(msg)

	messagesBuf := make(chan string, 2)

	messagesBuf <- "buffered"
	messagesBuf <- "channel"

	fmt.Println(<-messagesBuf)
	fmt.Println(<-messagesBuf)

	done := make(chan bool)
	go worker(done)

	<-done

	pings := make(chan string, 1)
	pongs := make(chan string, 1)
	ping(pings, "passed message")
	pong(pings, pongs)
	fmt.Println(<-pongs)
}

多路複用-Select

  • select是一種go可以處理多個通道之間的機制,看起來和switch語句很相似,但是select其實和IO機制中的select一樣,多路複用通道,隨機選取一個進行執行,如果說通道(channel)實現了多個goroutine之間的同步或者通訊,那麼select則實現了多個通道(channel)的同步或者通訊,並且select具有阻塞的特性。

  • select 是 Go 中的一個控制結構,類似於用於通訊的 switch 語句。每個 case 必須是一個通訊操作,要麼是傳送要麼是接收。

  • select 隨機執行一個可執行的 case,如果沒有 case 可執行,它將阻塞,直到有 case 可執行。一個預設的子句應該總是可執行的。

  • 當有多個通道等待接收資訊時,可以使用該select語句,並且希望在其中任何一個通道首先完成時執行一個動作。Go的select允許等待多個通道操作,將gooutine和channel與select結合是Go的一個強大功能。

package main

import (
	"fmt"
	"time"
)

func main() {

	c1 := make(chan string)
	c2 := make(chan string)

	go func() {
		time.Sleep(1 * time.Second)
		c1 <- "one"
	}()
	go func() {
		time.Sleep(2 * time.Second)
		c2 <- "two"
	}()

	for i := 0; i < 2; i++ {
		select {
		case msg1 := <-c1:
			fmt.Println("received", msg1)
		case msg2 := <-c2:
			fmt.Println("received", msg2)
		}
	}
}

通道使用

超時-Timeout

對於連線到外部資源或需要限制執行時間的程式來說超時非常重要。在Go 通道和select中實現超時是簡單且優雅的。

package main

import (
    "fmt"
    "time"
)

func main() {

    c1 := make(chan string, 1)
    go func() {
        time.Sleep(2 * time.Second)
        c1 <- "result 1"
    }()

    select {
    case res := <-c1:
        fmt.Println(res)
    case <-time.After(1 * time.Second):
        fmt.Println("timeout 1")
    }

    c2 := make(chan string, 1)
    go func() {
        time.Sleep(2 * time.Second)
        c2 <- "result 2"
    }()
    select {
    case res := <-c2:
        fmt.Println(res)
    case <-time.After(3 * time.Second):
        fmt.Println("timeout 2")
    }
}

非阻塞通道操作

通道上的基本傳送和接收阻塞,但可以使用帶有預設子句的select來實現非阻塞傳送、接收,甚至非阻塞多路選擇。無阻塞的接收如果訊息上有一個可用的值,那麼select將使用該值的<-messages情況;如果沒有可用的值則立即採用預設情況。非阻塞傳送的工作原理類似這裡不能將msg傳送到訊息通道,因為該通道沒有緩衝區,也沒有接收器,因此選擇預設情況。可以在預設子句之上使用多種情況來實現多路非阻塞選擇,對訊息和訊號進行非阻塞接收。

package main

import "fmt"

func main() {
	messages := make(chan string)
	signals := make(chan bool)

	select {
	case msg := <-messages:
		fmt.Println("received message", msg)
	default:
		fmt.Println("no message received")
	}

	msg := "hi"
	select {
	case messages <- msg:
		fmt.Println("sent message", msg)
	default:
		fmt.Println("no message sent")
	}

	select {
	case msg := <-messages:
		fmt.Println("received message", msg)
	case sig := <-signals:
		fmt.Println("received signal", sig)
	default:
		fmt.Println("no activity")
	}
}

關閉通道

關閉通道表示不再在該通道上傳送任何值,可用於完成通訊傳送給通道的接收器。

package main

import "fmt"

func main() {
	jobs := make(chan int, 5)
	done := make(chan bool)

	go func() {
		for {
			j, more := <-jobs
			if more {
				fmt.Println("received job", j)
			} else {
				fmt.Println("received all jobs")
				done <- true
				return
			}
		}
	}()

	for j := 1; j <= 3; j++ {
		jobs <- j
		fmt.Println("sent job", j)
	}
	close(jobs)
	fmt.Println("sent all jobs")

	<-done
}

通道迭代

上一篇基礎實戰中介紹使用for和range如何提供對基本資料結構的迭代,在這裡可以使用該range語法迭代從通道接收的值。

package main

import "fmt"

func main() {

    queue := make(chan string, 2)
    queue <- "one"
    queue <- "two"
    close(queue)

    for elem := range queue {
        fmt.Println(elem)
    }
}

定時器-TimerAndTicker

經常實際專案有不少需求需要使用在將來的某個時間點執行Go程式碼,或者在某個時間間隔重複執行;Go內建的定時器就能很簡單實現這個功能。GO標準庫中的定時器主要有兩種,一種為Timer定時器,一種為Ticker定時器。Timer計時器使用一次後,就失效了,需要Reset()才能再次生效,而Ticker計時器會一直生效。在一個GO程序中,其中的所有計時器都是由一個執行著 timerproc() 函數的 goroutine 來保護。它使用時間堆(最小堆)的演演算法來保護所有的 Timer,其底層的資料結構基於陣列的最小堆,堆頂的元素是間隔超時最近的 Timer,這個 goroutine 會定期 wake up,讀取堆頂的 Timer,執行對應的 f 函數或者 sendtime()函數,而後將其從堆頂移除。Timer資料結構如下:

package main

import (
	"fmt"
	"time"
)

func main() {

	timer1 := time.NewTimer(2 * time.Second)

	<-timer1.C
	fmt.Println("Timer 1 fired")

	timer2 := time.NewTimer(time.Second)
	go func() {
		<-timer2.C
		fmt.Println("Timer 2 fired")
	}()
	stop2 := timer2.Stop()
	if stop2 {
		fmt.Println("Timer 2 stopped")
	}

	time.Sleep(2 * time.Second)

	ticker := time.NewTicker(500 * time.Millisecond)
	done := make(chan bool)

	go func() {
		for {
			select {
			case <-done:
				return
			case t := <-ticker.C:
				fmt.Println("Tick at", t)
			}
		}
	}()

	time.Sleep(1600 * time.Millisecond)
	ticker.Stop()
	done <- true
	fmt.Println("Ticker stopped")
}

速率限制是控制資源利用和保持服務質量的重要機制。Go優雅地支援用 goroutines、channels和tickers來實現限制速率。

package main

import (
    "fmt"
    "time"
)

func main() {

    requests := make(chan int, 5)
    for i := 1; i <= 5; i++ {
        requests <- i
    }
    close(requests)

    limiter := time.Tick(200 * time.Millisecond)

    for req := range requests {
        <-limiter
        fmt.Println("request", req, time.Now())
    }

    burstyLimiter := make(chan time.Time, 3)

    for i := 0; i < 3; i++ {
        burstyLimiter <- time.Now()
    }

    go func() {
        for t := range time.Tick(200 * time.Millisecond) {
            burstyLimiter <- t
        }
    }()

    burstyRequests := make(chan int, 5)
    for i := 1; i <= 5; i++ {
        burstyRequests <- i
    }
    close(burstyRequests)
    for req := range burstyRequests {
        <-burstyLimiter
        fmt.Println("request", req, time.Now())
    }
}

工作池-Worker Pools

工作池是一種常用的並行設計模式,它利用一組固定數量的 goroutine 來處理一組任務。任務可以被非同步地新增到工作池中,等待可用的 worker goroutine 來處理。當沒有更多的任務需要處理時,worker goroutine 將會保持空閒狀態,等待新的任務到來。 在 Go 中,我們可以使用通道和 Goroutine 來實現這種模式

package main

import (
	"fmt"
	"time"
)

func worker(id int, jobs <-chan int, results chan<- int) {
	for j := range jobs {
		fmt.Println("worker", id, "started  job", j)
		time.Sleep(time.Second)
		fmt.Println("worker", id, "finished job", j)
		results <- j * 2
	}
}

func main() {

	const numJobs = 5
	jobs := make(chan int, numJobs)
	results := make(chan int, numJobs)

	for w := 1; w <= 3; w++ {
		go worker(w, jobs, results)
	}

	for j := 1; j <= numJobs; j++ {
		jobs <- j
	}
	close(jobs)

	for a := 1; a <= numJobs; a++ {
		<-results
	}
}

等待組-WaitGroup

在Go語言中,sync包下的WaitGroup結構體物件用於等待一組執行緒的結束;WaitGroup是go並行中最常用的工具,可以通過WaitGroup來表達這一組協程的任務是否完成,以決定是否繼續往下走,或者取任務結果。WaitGroup資料結構如下:

package main

import (
	"fmt"
	"sync"
	"time"
)

func worker(id int) {
	fmt.Printf("Worker %d starting\n", id)

	time.Sleep(time.Second)
	fmt.Printf("Worker %d done\n", id)
}

func main() {

	var wg sync.WaitGroup

	for i := 1; i <= 5; i++ {
		wg.Add(1)

		i := i

		go func() {
			defer wg.Done()
			worker(i)
		}()
	}

	wg.Wait()

}

原子操作-Atomic

  • 定義:原子操作即是進行過程中不能被中斷的操作;也就是說,針對某個值的原子操作在被進行的過程當中,CPU絕不會再去進行其它的針對該值的操作。為了實現這樣的嚴謹性,原子操作僅會由一個獨立的CPU指令代表和完成。只有這樣才能夠在並行環境下保證原子操作的絕對安全。
  • Go支援操作型別:int32、int64、uint32、uint64、uintptr和unsafe.Pointer型別。
  • Go原子操作:
    • 增或減:被用於進行增或減的原子操作(以下簡稱原子增/減操作)的函數名稱都以「Add」為字首,並後跟針對的具體型別的名稱。例如,實現針對uint32型別的原子增/減操作的函數的名稱為AddUint32。事實上,sync/atomic包中的所有函數的命名都遵循此規則。
    • 比較並交換:Compare And Swap,簡稱CAS。在sync/atomic包中,這類原子操作由名稱以「CompareAndSwap」為字首的若干個函數代表。
    • 載入:為了原子的讀取某個值,sync/atomic程式碼包同樣為我們提供了一系列的函數。這些函數的名稱都以「Load」為字首,意為載入。
    • 儲存:與讀取操作相對應的是寫入操作。而sync/atomic包也提供了與原子的值載入函數相對應的原子的值儲存函數。這些函數的名稱均以「Store」為字首。
    • 交換:在sync/atomic程式碼包中還存在著一類函數。它們的功能與前文所講的CAS操作和原子載入操作都有些類似。這樣的功能可以被稱為原子交換操作。這類函數的名稱都以「Swap」為字首。

Go語言提供的原子操作都是非侵入式的。它們由標準庫程式碼包sync/atomic中的眾多函數代表。可以通過呼叫這些函數對幾種簡單的型別的值進行原子操作。Go中管理狀態的主要機制是通過通道進行通訊,下面演示使用sync/atomic包來處理由多個執行緒例程存取的原子計數器。

package main

import (
    "fmt"
    "sync"
    "sync/atomic"
)

func main() {

    var ops uint64

    var wg sync.WaitGroup

    for i := 0; i < 50; i++ {
        wg.Add(1)

        go func() {
            for c := 0; c < 1000; c++ {

                atomic.AddUint64(&ops, 1)
            }
            wg.Done()
        }()
    }

    wg.Wait()

    fmt.Println("ops:", ops)
}

互斥鎖-Mutex

Go sync包提供了兩種鎖型別:互斥鎖sync.Mutex 和 讀寫互斥鎖sync.RWMutex,都屬於悲觀鎖。Mutex是互斥鎖,當一個 goroutine 獲得了鎖後,其他 goroutine 不能獲取鎖(只能存在一個寫或讀,不能同時讀和寫)。應用於多個執行緒同時存取臨界區],為保證資料的安全,鎖住一些共用資源, 以防止並行存取這些共用資料時可能導致的資料不一致問題。資料結構如下:

state表示鎖的狀態,有鎖定、被喚醒、飢餓模式等,並且是用state的二進位制位來標識的,不同模式下會有不同的處理方式。sema表示號誌,mutex阻塞佇列的定位是通過這個變數來實現的,從而實現goroutine的阻塞和喚醒。鎖的實現一般會依賴於原子操作、號誌,通過atomic 包中的一些原子操作來實現鎖的鎖定,通過號誌來實現執行緒的阻塞與喚醒。

package main

import (
	"fmt"
	"sync"
)

type Container struct {
	mu       sync.Mutex
	counters map[string]int
}

func (c *Container) inc(name string) {

	c.mu.Lock()
	defer c.mu.Unlock()
	c.counters[name]++
}

func main() {
	c := Container{

		counters: map[string]int{"a": 0, "b": 0},
	}

	var wg sync.WaitGroup

	doIncrement := func(name string, n int) {
		for i := 0; i < n; i++ {
			c.inc(name)
		}
		wg.Done()
	}

	wg.Add(3)
	go doIncrement("a", 10000)
	go doIncrement("a", 10000)
	go doIncrement("b", 10000)

	wg.Wait()
	fmt.Println(c.counters)
}

讀寫互斥鎖-RWMutex

讀寫互斥鎖RWMutex,是對Mutex的一個擴充套件,當一個 goroutine 獲得了讀鎖後,其他 goroutine可以獲取讀鎖,但不能獲取寫鎖;當一個 goroutine 獲得了寫鎖後,其他 goroutine既不能獲取讀鎖也不能獲取寫鎖(只能存在一個寫或多個讀,可以同時讀)。常用於讀多於寫的情況(既保證執行緒安全,又保證效能不太差)。資料結構如下:

  • 讀寫鎖區分讀者和寫者,而互斥鎖不區分。
  • 互斥鎖同一時間只允許一個執行緒存取該物件,無論讀寫;讀寫鎖同一時間內只允許一個寫者,但是允許多個讀者同時讀物件
package main

import (
	"fmt"
	"sync"
	"time"
)

type Counter struct {
	value   int
	rwMutex sync.RWMutex
}

func (c *Counter) GetValue() int {
	c.rwMutex.RLock()
	defer c.rwMutex.RUnlock()
	return c.value
}

func (c *Counter) Increment() {
	c.rwMutex.Lock()
	defer c.rwMutex.Unlock()
	c.value++
}
func main() {
	counter := Counter{value: 0}

	// 讀操作
	for i := 0; i < 10; i++ {
		go func() {
			for {
				fmt.Println("Value: ", counter.GetValue())
				time.Sleep(time.Millisecond)
			}
		}()
	}

	// 寫操作
	for {
		counter.Increment()
		time.Sleep(time.Second)
	}
}

有狀態協程

在前面的例子中,我們使用了帶有互斥鎖的顯式鎖來同步跨多個執行緒對共用狀態的存取。而另一種選擇是使用程式和通道的內建同步特性來實現相同的結果。這種基於通道的方法符合Go的思想,即通過通訊共用記憶體,並使每個資料塊由一個執行緒程式擁有。

package main

import (
    "fmt"
    "math/rand"
    "sync/atomic"
    "time"
)

type readOp struct {
    key  int
    resp chan int
}
type writeOp struct {
    key  int
    val  int
    resp chan bool
}

func main() {

    var readOps uint64
    var writeOps uint64

    reads := make(chan readOp)
    writes := make(chan writeOp)

    go func() {
        var state = make(map[int]int)
        for {
            select {
            case read := <-reads:
                read.resp <- state[read.key]
            case write := <-writes:
                state[write.key] = write.val
                write.resp <- true
            }
        }
    }()

    for r := 0; r < 100; r++ {
        go func() {
            for {
                read := readOp{
                    key:  rand.Intn(5),
                    resp: make(chan int)}
                reads <- read
                <-read.resp
                atomic.AddUint64(&readOps, 1)
                time.Sleep(time.Millisecond)
            }
        }()
    }

    for w := 0; w < 10; w++ {
        go func() {
            for {
                write := writeOp{
                    key:  rand.Intn(5),
                    val:  rand.Intn(100),
                    resp: make(chan bool)}
                writes <- write
                <-write.resp
                atomic.AddUint64(&writeOps, 1)
                time.Sleep(time.Millisecond)
            }
        }()
    }

    time.Sleep(time.Second)

    readOpsFinal := atomic.LoadUint64(&readOps)
    fmt.Println("readOps:", readOpsFinal)
    writeOpsFinal := atomic.LoadUint64(&writeOps)
    fmt.Println("writeOps:", writeOpsFinal)
}

單執行-Once

Once 是 Go 內建庫 sync 中一個比較簡單的並行原語;顧名思義,它的作用就是執行那些只需要執行一次的動作。

package main

import (
	"fmt"
	"sync"
)

func main() {
	var once sync.Once
	onceBody := func() {
		fmt.Println("Only once")
	}
	done := make(chan bool)
	for i := 0; i < 10; i++ {
		go func() {
			once.Do(onceBody)
			done <- true
		}()
	}
	for i := 0; i < 10; i++ {
		<-done
	}
}

Once 最典型的使用場景就是單例物件的初始化,類似思想如在 MySQL 或者 Redis 這種頻繁存取資料的場景中,建立連線的代價遠遠高於資料讀寫的代價,因此我們會用單例模式來實現一次建立連線,多次存取資料,從而提升服務效能。

package main

import (
    "net"
    "sync"
    "time"
)

// 使用互斥鎖保證執行緒(goroutine)安全
var connMu sync.Mutex
var conn net.Conn

func getConn() net.Conn {
    connMu.Lock()
    defer connMu.Unlock()

    // 返回已建立好的連線
    if conn != nil {
        return conn
    }

    // 建立連線
    conn, _ = net.DialTimeout("tcp", "baidu.com:80", 10*time.Second)
    return conn
}

// 使用連線
func main() {
    conn := getConn()
    if conn == nil {
        panic("conn is nil")
    }
}

條件-Cond

Go 標準庫提供 Cond 原語的目的是,為等待 / 通知場景下的並行問題提供支援。Cond通常應用於等待某個條件的一組goroutine,等條件變為true的時候,其中一個goroutine或者所有的goroutine都會被喚醒執行。開發實踐中使用到Cond場景比較少,且Cond場景一般也能用Channel方式實現,所以更多人會選擇使用Channel。

package main

import (
	"fmt"
	"sync"
	"time"
)

var (
	// 1. 定義一個互斥鎖
	mu    sync.Mutex
	cond  *sync.Cond
	count int
)

func init() {
	// 2.將互斥鎖和sync.Cond進行關聯
	cond = sync.NewCond(&mu)
}

func worker(id int) {
	// 消費者
	for {
		// 3. 在需要等待的地方,獲取互斥鎖,呼叫Wait方法等待被通知
		mu.Lock()
		// 這裡會不斷迴圈判斷 是否有待消費的任務
		for count == 0 {
			cond.Wait() // 等待任務
		}
		count--
		fmt.Printf("worker %d: 處理了一個任務", id)
		// 5. 最後釋放鎖
		mu.Unlock()
	}
}

func main() {
	// 啟動5個消費者
	for i := 1; i <= 5; i++ {
		go worker(i)
	}

	for {
		// 生產者
		time.Sleep(1 * time.Second)
		mu.Lock()
		count++
		// 4. 在需要等待的地方,獲取互斥鎖,呼叫BroadCast/Singal方法進行通知
		cond.Broadcast()
		mu.Unlock()
	}
}

上下文-Context

定義:Golang 的 Context 應用開發常用的並行控制工具,用於在程式中的 API 層或程序之間共用請求範圍的資料、取消訊號以及超時或截止日期。Context 又被稱為上下文,與 WaitGroup 不同的是,context 對於派生 goroutine 有更強的控制力,可以管理多級的 goroutine。context包的核心原理,鏈式傳遞context,基於context構造新的context。下面是http的上下文範例:

package main

import (
    "fmt"
    "net/http"
    "time"
)

func hello(w http.ResponseWriter, req *http.Request) {

    ctx := req.Context()
    fmt.Println("server: hello handler started")
    defer fmt.Println("server: hello handler ended")

    select {
    case <-time.After(10 * time.Second):
        fmt.Fprintf(w, "hello\n")
    case <-ctx.Done():

        err := ctx.Err()
        fmt.Println("server:", err)
        internalError := http.StatusInternalServerError
        http.Error(w, err.Error(), internalError)
    }
}

func main() {

    http.HandleFunc("/hello", hello)
    http.ListenAndServe(":8090", nil)
}
# 存取http的介面
curl http://localhost:8090/hello

訊號-signal

訊號是事件發生時對程序的通知機制。有時也稱之為軟體中斷。訊號與硬體中斷的相似之處在於打斷了程式執行的正常流程,大多數情況下,無法預測訊號到達的精確時間。有時希望Go程式能夠智慧地處理Unix訊號;例如希望伺服器在接收到SIGTERM時優雅地關閉,或者希望命令列工具在接收到SIGINT時停止處理輸入。Go程式無法捕獲訊號 SIGKILL 和 SIGSTOP (終止和暫停程序),因此 os/signal 包對這兩個訊號無效。

package main

import (
    "fmt"
    "os"
    "os/signal"
    "syscall"
)

func main() {

    sigs := make(chan os.Signal, 1)

    signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)

    done := make(chan bool, 1)

    go func() {

        sig := <-sigs
        fmt.Println()
        fmt.Println(sig)
        done <- true
    }()

    fmt.Println("awaiting signal")
    <-done
    fmt.Println("exiting")
}

Pool

go提供的sync.Pool是為了物件的複用,如果某些物件的建立比較頻繁,就把他們放入Pool中快取起來以便使用,這樣重複利用記憶體,減少GC的壓力,Go同步包中,sync.Pool提供了儲存和存取一組臨時物件並複用它們的能力。

對於一些建立成本昂貴、頻繁使用的臨時物件,使用sync.Pool可以減少記憶體分配,降低GC壓力。因為Go的gc演演算法是根據標記清除改進的三色標記法,如果頻繁建立大量臨時物件,勢必給GC標記帶來負擔,CPU也很容易出現毛刺現象。當然需要注意的是:儲存在Pool中的物件隨時都可能在不被通知的情況下被移除。所以並不是所有頻繁使用、建立昂貴的物件都適用,比如DB連線、執行緒池。

package main

import "sync"

type Person struct {
	Age int
}

// 初始化pool
var personPool = sync.Pool{
	New: func() interface{} {
		return new(Person)
	},
}

func main() {
	// 獲取一個範例
	newPerson := personPool.Get().(*Person)
	// 回收物件 以備其他協程使用
	defer personPool.Put(newPerson)

	newPerson.Age = 25
}

執行緒安全Map

Go中自己通過make建立的map不是執行緒安全的,Go為了解決這個問題,專門給我們提供了一個並行安全的map,這個並行安全的map不用通過make建立,拿來即可用,並且提供了一些不同於普通map的操作方法。

package main

import (
	"fmt"
	"sync"
)

// 建立一個sync包下的執行緒安全map物件
var myConcurrentMap = sync.Map{}

// 遍歷資料用的
var myRangeMap = sync.Map{}

func main() {
	//儲存資料
	myConcurrentMap.Store(1, "li_ming")
	//取出資料
	name, ok := myConcurrentMap.Load(1)
	if !ok {
		fmt.Println("不存在")
		return
	}
	//列印值  li_ming
	fmt.Println(name)
	//該key有值,則ok為true,返回它原來存在的值,不做任何操作;該key無值,則執行新增操作,ok為false,返回新新增的值
	name2, ok2 := myConcurrentMap.LoadOrStore(1, "xiao_hong")
	//因為key=1存在,所以列印是   li_ming true
	fmt.Println(name2, ok2)
	name3, ok3 := myConcurrentMap.LoadOrStore(2, "xiao_hong")
	//因為key=2不存在,所以列印是   xiao_hong false
	fmt.Println(name3, ok3)
	//標記刪除值
	myConcurrentMap.Delete(1)
	//取出資料
	//name4,ok4 := myConcurrentMap.Load(1)
	//if(!ok4) {
	// fmt.Println("name4=不存在")
	// return
	//}
	//fmt.Println(name4)

	//遍歷資料
	rangeFunc()
}

// 遍歷
func rangeFunc() {
	myRangeMap.Store(1, "xiao_ming")
	myRangeMap.Store(2, "xiao_li")
	myRangeMap.Store(3, "xiao_ke")
	myRangeMap.Store(4, "xiao_lei")

	myRangeMap.Range(func(k, v interface{}) bool {
		fmt.Println("data_key_value = :", k, v)
		//return true代表繼續遍歷下一個,return false代表結束遍歷操作
		return true
	})

}

  • 本人部落格網站IT小神 www.itxiaoshen.com