golang

golang

  1. go和php的區別

    • 類型:go為編譯性語言;php解釋性語言

    • 錯誤:go的錯誤處理機制;php本身或者框架即可糾錯

    • 性能:go重視并發性能 php重視開發速度

    • 應用:go側重于容器/高性能并發/云計算/;php側重于后臺/網站/系統

  2. 編譯型代碼和解釋型代碼的區別

    • 編譯型和解釋型語言的不同:過程發生的時機不一樣。
    • 編譯型語言的代表是C,源代碼被編譯之后生成中間文件(.o和.obj),然后用連接器和匯編器生成機器碼,也就是一系列基本操作的序列,機器碼最后被執行生成最終動作。
    • 解釋型的語言以Ruby為例,也經歷了這些步驟,不同的是,C語言會把那些從源代碼“變”來的基本操作序列(保存)起來,而Ruby直接將這些生成的基本操作序列(Ruby虛擬機)指令丟給Ruby虛擬機執行然后產生動作了。
  3. go的鎖:

    一旦數據被多個線程共享,那么就很可能會產生爭用和沖突的情況。這種情況也被稱為競態條件(race condition),這往往會破壞共享數據的一致性。

    一個互斥鎖可以被用來保護一個臨界區或者一組相關臨界區。我們可以通過它來保證,在同一時刻只有一個 goroutine 處于該臨界區之內。為了兌現這個保證,每當有 goroutine 想進入臨界區時,都需要先對它進行鎖定,并且,每個 goroutine 離開臨界區時,都要及時地對它進行解鎖。

    sync包中的Mutex就是與其對應的類型,該類型的值可以被稱為互斥量或者互斥鎖。互斥鎖是開箱即用的。換句話說,一旦我們聲明了一個sync.Mutex類型的變量,就可以直接使用它了。

    保證多個 goroutine 并發地訪問同一個共享資源時的完全串行,這是通過保護針對此共享資源的一個臨界區,或一組相關臨界區實現的。因此,我們可以把它看做是 goroutine 進入相關臨界區時,必須拿到的訪問令牌。

    • 使用互斥鎖的注意事項如下:

    不要重復鎖定互斥鎖;

    不要忘記解鎖互斥鎖,必要時使用defer語句;

    不要對尚未鎖定或者已解鎖的互斥鎖解鎖;

    不要在多個函數之間直接傳遞互斥鎖。

    <img src="https://static001.geekbang.org/resource/image/73/6c/73d3313640e62bb95855d40c988c2e6c.png" alt="img" style="zoom:35%;" />

  1. go的interface是怎么實現的?

    只要目標類型方法集內包含接口聲明的全部方法。

    當我們給一個接口變量賦值的時候,該變量的動態類型會與它的動態值一起被存儲在一個專用的數據結構中。iface的實例會包含兩個指針,一個是指向類型信息的指針,另一個是指向動態值的指針。這里的類型信息是由另一個專用數據結構的實例承載的,其中包含了動態值的類型,以及使它實現了接口的方法和調用它們的途徑,等等。總之,接口變量被賦予動態值的時候,存儲的是包含了這個動態值的副本的一個結構更加復雜的值。

    一個簡單的邏輯就是需要獲取這個類型的所有方法集合(集合A),并獲取該接口包含的所有方法集合(集合B),然后判斷列表B是否為列表A的子集。

  2. golang的并發模式

    goroutine是go的輕量級線程實現。

    CSP (Communicating Sequential Process,通訊順序進程) 模型不同于傳統的多線程通過共享內存來通信,CSP講究的是“以通信的方式來共享內存”。用于描述兩個獨立的并發實體通過共享的通訊 channel(管道)進行通信的并發模型。 CSP中channel是第一類對象,它不關注發送消息的實體,而關注與發送消息時使用的channel。

    • 通過channel通知實現并發控制

      func main() {
          ch := make(chan struct{})
          go func() {
              fmt.Println("do something..")
              time.Sleep(time.Second * 1)
              ch <- struct{}{}
          }()
      
          <-ch
      
          fmt.Println("I am finished")
      }
      
    • 通過sync包中的WaitGroup實現并發控制

      sync 包中,提供了 WaitGroup ,它會等待它收集的所有 goroutine 任務全部完成。在WaitGroup里主要有三個方法

      • Add, 可以添加或減少 goroutine的數量
      • Done, 相當于Add(-1)
      • Wait, 執行后會堵塞主線程,直到WaitGroup 里的值減至0
      func main(){
          var wg sync.WaitGroup
          var urls = []string{
              "http://www.golang.org/",
              "http://www.google.com/",
              "http://www.somestupidname.com/",
          }
          for _, url := range urls {
              wg.Add(1)
              go func(url string) {
                  defer wg.Done()
                  http.Get(url)
              }(url)
          }
          wg.Wait()
      }
      
    • Context。

      goroutine 的上下文。它是包括一個程序的運行環境、現場和快照等。每個程序要運行時,都需要知道當前程序的運行狀態,通常Go 將這些封裝在一個 Context 里,再將它傳給要執行的 goroutine

      在每一個循環中產生一個goroutine,每一個goroutine中都傳入context,在每個goroutine中通過傳入ctx創建一個Context,并且通過select一直監控該Context的運行情況,當在父Context退出的時候,代碼中并沒有明顯調用子ContextCancel函數,但是分析結果,子Context還是被正確合理的關閉了,這是因為,所有基于這個Context或者衍生的子Context都會收到通知,這時就可以進行清理操作了,最終釋放goroutine,這就優雅的解決了goroutine啟動后不可控的問題。

      package main
      
      import (
          "context"
          "fmt"
          "sync"
          "time"
      )
      
      type Message struct {
          netId int
          Data  string
      }
      
      type ServerConn struct {
          sendCh   chan Message
          handleCh chan Message
          wg       *sync.WaitGroup
          ctx      context.Context
          cancel   context.CancelFunc
          netId    int
      }
      
      func main() {
      
          conn := &ServerConn{
              sendCh:   make(chan Message),
              handleCh: make(chan Message),
              wg:       &sync.WaitGroup{},
              netId:    100,
          }
      
          conn.ctx, conn.cancel = context.WithCancel(context.WithValue(context.Background(), "key", conn.netId))
          loopers := []func(*ServerConn, *sync.WaitGroup){readLoop, writeLoop, handleLoop}
      
          for _, looper := range loopers {
              conn.wg.Add(1)
              go looper(conn, conn.wg)
          }
      
          go func() {
              time.Sleep(time.Second * 3)
              conn.cancel()
          }()
      
          conn.wg.Wait()
      
      }
      
      func readLoop(c *ServerConn, wg *sync.WaitGroup) {
      
          netId, _ := c.ctx.Value("key").(int)
          handlerCh := c.handleCh
          ctx, _ := context.WithCancel(c.ctx)
          cDone := ctx.Done()
      
          defer wg.Done()
      
          for {
              time.Sleep(time.Second * 1)
              select {
              case <-cDone:
                  fmt.Println("readLoop close")
                  return
              default:
                  handlerCh <- Message{netId, "Hello world"}
              }
          }
      }
      
      func handleLoop(c *ServerConn, wg *sync.WaitGroup) {
          handlerCh := c.handleCh
          sendCh := c.sendCh
          ctx, _ := context.WithCancel(c.ctx)
          cDone := ctx.Done()
      
          defer wg.Done()
      
          for {
              select {
              case handleData, ok := <-handlerCh:
                  if ok {
                      handleData.netId++
                      handleData.Data = "I am whole world"
                      sendCh <- handleData
                  }
      
              case <-cDone:
                  fmt.Println("handleLoop close")
                  return
              }
      
          }
      }
      
      func writeLoop(c *ServerConn, wg *sync.WaitGroup) {
          sendCh := c.sendCh
          ctx, _ := context.WithCancel(c.ctx)
          cDone := ctx.Done()
      
          defer wg.Done()
      
          for {
              select {
              case sendData, ok := <-sendCh:
                  if ok {
                      fmt.Println(sendData)
                  }
              case <-cDone:
                  fmt.Println("writeLoop close")
                  return
              }
          }
      }
      
  3. goroutine、channel。

    Goroutine是Go中最基本的執行單元。事實上每一個Go程序至少有一個goroutine:主goroutine。當程序啟動時,它會自動創建。goroutine是Go語言的基本調度單位,而channel則是它們之間的通信機制。操作符<-用來指定管道的方向,發送或接收。

    Go 語言里的并發指的是能讓某個函數獨立于其他函數運行的能力。當一個函數創建為 goroutine 時,Go 會將其視為一個獨立的工作單元。這個單元會被調度到可用的邏輯處理器上執行。Go 語言 運行時的調度器是一個復雜的軟件,能管理被創建的所有 goroutine 并為其分配執行時間。這個調度 器在操作系統之上,將操作系統的線程與語言運行時的邏輯處理器綁定,并在邏輯處理器上運行 goroutine。調度器在任何給定的時間,都會全面控制哪個 goroutine 要在哪個邏輯處理器上運行。

    Go 語言的并發同步模型來自一個叫作通信順序進程(Communicating Sequential Processes,CSP) 的范型(paradigm)。CSP 是一種消息傳遞模型,通過在 goroutine 之間傳遞數據來傳遞消息,而不是 對數據進行加鎖來實現同步訪問。用于在 goroutine 之間同步和傳遞數據的關鍵數據類型叫作通道(channel)。對于沒有使用過通道寫并發程序的程序員來說,通道會讓他們感覺神奇而興奮。希望讀 者使用后也能有這種感覺。使用通道可以使編寫并發程序更容易,也能夠讓并發程序出錯更少。

  4. 線程和協程

    線程:

    當運行一個應用程序的時候,操作系統會給這個應用程序啟動一個進程。我們可以將進程看作一個包含應用程序在運行中需要用到和維護的各種資源的容器。一個進程至少包含一個線程,這個線程就是主線程。操作系統會調度線程到不同的CPU上執行,這個CPU不一定就是進程所在的CPU。

    <img src="/Users/anyao/Library/Application Support/typora-user-images/image-20200402232450525.png" alt="image-20200402232450525" style="zoom:40%;" />

    • 進程:資源的所有權
    • 線程:執行和調度的基本單位
    • 同一進程下的各個線程共享資源,但寄存器、棧、PC不共享

    協程:也有人稱之為輕量級線程,具備以下幾個特點:

    • 能夠在單一的系統線程中模擬多個任務的并發執行。
    • 在一個特定的時間,只有一個任務在運行,即并非真正地并行。
    • 被動的任務調度方式,即任務沒有主動搶占時間片的說法。當一個任務正在執行時,外部沒有辦法中止它。要進行任務切換,只能通過由該任務自身調用yield()來主動出讓 CPU使用權。
    • 每個協程都有自己的堆棧和局部變量。

    每個協程都包含3種運行狀態:掛起、運行和停止。停止通常表示該協程已經執行完成。每個協程都包含3種運行狀態:掛起、運行和停止。停止通常表示該協程已經執行完成(包括遇到問題明確執行退出),掛起則表示該協程尚未執行完成,但出讓了時間片,以后 有機會時會由調度器繼續執行。

    線程與協程的區別:

    • 可控的切換時機,一旦創建完線程,你就無法決定他什么時候獲得時間片,什么時候讓出時間片了,你把它交給了內核。而協程可以有。
    • 很小的切換代價,從操作系統有沒有調度權上看,協程就是因為不需要進行內核態的切換,所以會使用它,會有這么個東西。
  5. Golang 的協程信通訊方式有哪些。

    • 全局共享變量
    • channel通信
    • Context包
  6. 垃圾回收機制

    • 三色標記法是對標記階段的改進
      1. 初始狀態所有對象都是白色。
      2. 從root根出發掃描所有根對象(下圖a,b),將他們引用的對象標記為灰色(圖中A,B)。root區域主要是程序運行到當前時刻的棧和全局數據區域。
      3. 分析灰色對象是否引用了其他對象。如果沒有引用其它對象則將該灰色對象標記為黑色(上圖中A);
      4. 如果有引用則將它變為黑色的同時將它引用的對象也變為灰色(上圖中B引用了D)
        重復步驟3,直到灰色對象隊列為空。此時白色對象即為垃圾,進行回收。

<img src="/Users/anyao/Library/Application Support/typora-user-images/image-20200403000810827.png" alt="image-20200403000810827" style="zoom:50%;" /><img src="/Users/anyao/Library/Application Support/typora-user-images/image-20200403000829012.png" alt="image-20200403000829012" style="zoom:50%;" />

<img src="/Users/anyao/Library/Application Support/typora-user-images/image-20200403000857955.png" alt="image-20200403000857955" style="zoom:50%;" /><img src="/Users/anyao/Library/Application Support/typora-user-images/image-20200403000921980.png" alt="image-20200403000921980" style="zoom:50%;" />

  • GC流程

    其實是因為Golang GC的大部分處理是和用戶代碼并行的。GC期間用戶代碼可能會改變某些對象的狀態,如何實現GC和用戶代碼并行呢?先看下GC工作的完整流程:

    Mark: 包含兩部分:

    Mark Prepare: 初始化GC任務,包括開啟寫屏障(write barrier)和輔助GC(mutator assist),統計root對象的任務數量等。這個過程需要STW

    GC Drains: 掃描所有root對象,包括全局指針和goroutine(G)棧上的指針(掃描對應G棧時需停止該G),將其加入標記隊列(灰色隊列),并循環處理灰色隊列的對象,直到灰色隊列為空。該過程后臺并行執行

    Mark Termination: 完成標記工作,重新掃描(re-scan)全局指針和棧。因為Mark和用戶程序是并行的,所以在Mark過程中可能會有新的對象分配和指針賦值,這個時候就需要通過寫屏障(write barrier)記錄下來,re-scan 再檢查一下。這個過程也是會STW的。

    Sweep: 按照標記結果回收所有的白色對象,該過程后臺并行執行

    Sweep Termination: 對未清掃的span進行清掃, 只有上一輪的GC的清掃工作完成才可以開始新一輪的GC。

如果標記期間用戶邏輯改變了剛打完標記的對象的引用狀態,怎么辦呢。

就是在每一輪GC開始時會初始化一個叫做“屏障”的東西,然后由它記錄第一次scan時各個對象的狀態,以便和第二次re-scan進行比對,引用狀態變化的對象被標記為灰色以防止丟失,將屏障前后狀態未變化對象繼續處理。

  • GC 觸發時機

    1. 超過內存大小閾值
    2. 達到定時時間 閾值是由一個gcpercent的變量控制的,當新分配的內存占已在使用中的內存的比例超過gcprecent時就會觸發。
  1. GPM并發調度

    <img src="/Users/anyao/Library/Application Support/typora-user-images/image-20200403003427220.png" alt="image-20200403003427220" style="zoom:40%;" />

    首先是 Processor(簡稱 P),其作用類似于 CPU 核,用來控制可同時并發執行的任務數量。每個工作線程都必須綁定一個有效 P 才被允許執行任務,否則只能休眠,直到有空閑 P 時被喚醒。P 還為線程提供執行資源,比如對象分配內存、本地任務隊列等。線程獨享所綁定的P資源,可在無鎖狀態下執行高效操作。

    基本上,進程內的一切都在以 goroutine(簡稱 G)方式運行,包括運行時相關服務,以及 main.main 入口函數。需要指出,G并非執行體,它僅僅保存并發任務狀態,為任務執行提供所需棧內存空間。G 任務創建后被放置在P本地隊列或全局隊列,等待工作線程調度執行。

    實際執行體是系統線程(簡稱 M),它和P綁定,以調度循環方式不停執行G 并發任務。M通過修改寄存器,將執行棧指向G自帶的棧內存,并在此空間內分配堆棧幀,執行任務函數。當需要中途切換時,只要將相關寄存器值保存回G空間即可維護狀態,任務M都可據此恢復執行。線程僅負責執行,不再持有狀態,這是并發任務跨線程調度,實現多路復用的根本所在。

    盡管 P/M 構成執行組合體,但兩者數量并非一一對應。通常情況下,P 的數量相對恒定,默認與CPU核數量相同,但也可能更多或更少,而M則是由調度器按需創建的。舉例來說,當M 因陷入系統調用而長時間阻塞時,P 就會被監控線程搶回,去新建(或喚醒)一個M執行其他任務,這樣M的數量就會增長。

    因為G初始棧僅有 2KB,且創建操作只是在用戶空間簡單地分配對象,遠比進入內核態分配線程要簡單得多。調度器讓多個M進入調度循環,不停獲取并執行任務,所以我們才能創建成千上萬個并發任務。

操作系統線程、邏輯處理器和本地運行隊列之間的關系。如果創建一個 goroutine 并準備運行,這個 goroutine 就會被放到調度器的全局運行隊列中。之后,調度器就將這些隊列中的 goroutine 分配給一個邏輯處理器,并放到這個邏輯處理器對應的本地運行隊列中。本地運行隊列中的 goroutine 會一直等待直到自己被分配的邏輯處理器執行。

<img src="/Users/anyao/Library/Application Support/typora-user-images/image-20200403003703442.png" alt="image-20200403003703442" style="zoom:40%;" />

  1. Golang 里的逃逸分析是什么?怎么避免內存逃逸?

    • 定義

    在編譯程序優化理論中,逃逸分析是一種確定指針動態范圍的方法,簡單來說就是分析在程序的哪些地方可以訪問到該指針。

    再往簡單的說,Go是通過在編譯器里做逃逸分析(escape analysis)來決定一個對象放棧上還是放堆上,不逃逸的對象放棧上,可能逃逸的放堆上;即我發現變量在退出函數后沒有用了,那么就把丟到棧上,畢竟棧上的內存分配和回收比堆上快很多;反之,函數內的普通變量經過逃逸分析后,發現在函數退出后變量還有在其他地方上引用,那就將變量分配在堆上。做到按需分配。

    • 存在目的

    堆(Heap):一般來講是人為手動進行管理,手動申請、分配、釋放。堆適合不可預知大小的內存分配,這也意味著為此付出的代價是分配速度較慢,而且會形成內存碎片。

    棧(Stack):由編譯器進行管理,自動申請、分配、釋放。一般不會太大,因此棧的分配和回收速度非常快;我們常見的函數參數(不同平臺允許存放的數量不同),局部變量等都會存放在棧上。

    棧分配內存只需要兩個CPU指令:“PUSH”和“RELEASE”,分配和釋放;而堆分配內存首先需要去找到一塊大小合適的內存塊,之后要通過垃圾回收才能釋放。

    通俗比喻的說,棧就如我們去飯館吃飯,只需要點菜(發出申請)--》吃吃吃(使用內存)--》吃飽就跑剩下的交給飯館(操作系統自動回收),而堆就如在家里做飯,大到家,小到買什么菜,每一個環節都需要自己來實現,但是自由度會大很多。

    我們就可以更好的知道逃逸分析存在的目的了:

    ? 1. 減少gc壓力,棧上的變量,隨著函數退出后系統直接回收,不需要gc標記后再清除。

    ? 2. 減少內存碎片的產生。

    ? 3. 減輕分配堆內存的開銷,提高程序的運行速度。

    • 避免內存逃逸

    不要盲目使用變量的指針作為函數參數,雖然它會減少復制操作。但其實當參數為變量自身的時候,復制是在棧上完成的操作,開銷遠比變量逃逸后動態地在堆上分配內存少的多。

  2. slice底層原理

    切片是一個很小的對象,對底層數組進行了抽象,并提供相關的操作方法。切片有 3 個字段 的數據結構,這些數據結構包含 Go 語言需要操作底層數組的元數據。這 3 個字段分別是指向底層數組的指針、切片訪問的元素的個數(即長度)和切片允許增長 到的元素個數(即容量)。后面會進一步講解長度和容量的區別。

    <img src="/Users/anyao/Library/Application Support/typora-user-images/image-20200403005146656.png" alt="image-20200403005146656" style="zoom:40%;" />

  3. Golang 的默認參數傳遞方式以及哪些是引用傳遞?

    默認采用值傳遞,且Go 中函數傳參僅有值傳遞一種方式。傳參方式。slice、map、channel 都是引用類型。

  4. for 和 for range 區別:for range 是值拷貝,隨機遍歷。

  5. golang的特點

    • 開發速度:使用了更加智能的編譯器,并簡化了解決依賴的算法,最終提供了更快的編譯速度。
    • 并發:在 goroutine 之間發送消息,而不是讓多個 goroutine 爭奪同一個數據的使用權。
    • 類型系統:提供了靈活的、無繼承的類型系統,無需降低運行性能就能最大程度上復用代碼。還具有獨特的接口實現機制,允許用戶對行為進行建模,而不是對類型進行建模。
    • 內存管理:使用內存前要先分配這段內存,而且使用完畢后要將其釋放掉。
  6. Go 如何靜態的去看線程是否安全:可以使用 go run -race 或者 go build -race來進行靜態檢測。

  7. 用戶態和內核態

    • 內核態:控制計算機的硬件資源,并提供上層應用程序運行的環境。

    • 用戶態:上層應用程序的活動空間,應用程序的執行必須依托于內核提供的資源。

    • 用戶態切換為內核態的三種情況:

      • 系統調用:為了使上層應用能夠訪問到這些資源,內核為上層應用提供訪問的接口。
      • 異常事件: 當CPU正在執行運行在用戶態的程序時,突然發生某些預先不可知的異常事件,這個時候就會觸發從當前用戶態執行的進程轉向內核態執行相關的異常事件,典型的如缺頁異常。
      • 外圍設備的中斷:當外圍設備完成用戶的請求操作后,會像CPU發出中斷信號,此時,CPU就會暫停執行下一條即將要執行的指令,轉而去執行中斷信號對應的處理程序,如果先前執行的指令是在用戶態下,則自然就發生從用戶態到內核態的轉換。
  8. channel緩沖的問題:帶有緩沖(buffer)channel則可以非阻塞容納N個元素。發送數據到緩沖(buffer) channel不會被阻塞,除非channel已滿;同樣的,從緩沖(buffer) channel取數據也不會被阻塞,除非channel空了。

  9. golang的select有多個case時如何選擇執行順序。

    • select機制簡述
      1. select+case是用于阻塞監聽goroutine的,如果沒有case,就單單一個select{},則為監聽當前程序中的goroutine,此時注意,需要有真實的goroutine在跑,否則select{}會報panic
      2. select底下有多個可執行的case,則隨機執行一個。
      3. select常配合for循環來監聽channel有沒有故事發生。需要注意的是在這個場景下,break只是退出當前select而不會退出for,需要用break TIP / goto的方式。
      4. 無緩沖的通道,則傳值后立馬close,則會在close之前阻塞,有緩沖的通道則即使close了也會繼續讓接收后面的值
      5. 同個通道多個goroutine進行關閉,可用recover panic的方式來判斷通道關閉問題
        看完以上知識點其實還是沒法解釋本文的核心疑惑,繼續往下!
go select思想來源于網絡IO模型中的select,本質上也是IO多路復用,只不過這里的IO是基于channel而不是基于網絡,同時go select也有一些自己不同的特性:

    1. 每個case都必須是一個通信
    2. 所有channel表達式都會被求值
    3. 所有被發送的表達式都會被求值
    4. 如果任意某個通信可以進行,它就執行;其他被忽略。
    5. 如果有多個case都可以運行,select會隨機公平地選出一個執行。其他不會執行。否則執行default子句(如果有)
    6. 如果沒有default字句,select將阻塞,直到某個通信可以運行;Go不會重新對channel或值進行求值。
  1. golang的變量分配

    • Data segment:

      • 初始化的全局變量或靜態變量,會被分配在 Data 段。
      • 未初始化的全局變量或靜態變量,會被分配在 BSS 段。
      • 在函數中定義的局部變量,會被分配在堆(Heap 段)或棧(Stack 段)。
      • 實際上,如果考慮到 編譯器優化,局部變量還可能會被 分配在寄存器,或者直接被 優化去掉。
    • 對于 Go 而言,有兩個地方可以用于分配:

      • 堆(heap):由 GC 負責回收。對應于進程地址空間的堆。

      • 棧(stack):不涉及 GC 操作。每個 goroutine 都有自己的棧,初始時被分配在進程地址空間的棧上,擴容時被分配在進程地址空間的堆上。

    • Go 變量主要分為兩種:

      • 全局變量:會被 Go 編譯器標記為一些特殊的 符號類型,分配在堆上還是棧上目前尚不清楚,不過不是本文討論的重點。

      • 局部變量:對于在函數中定義的 Go 局部變量:要么被分配在堆上,要么被分配在棧上。

- Go 編譯器會盡可能將變量分配在棧上,以下兩種情況,Go 編譯器會將變量分配在堆上:
  - 如果一個變量被取地址(has its address taken),并且被逃逸分析(escape analysis)識別為 “逃逸到堆”(escapes to heap)
  - 如果一個變量很大(very large)
- 結論:

  - make([]struct{}, n) 只會被分配在棧上,而不會被分配在堆上。
  - Brad Fitzpatrick 的注釋是對的,并且他的意思是 “不會引發堆分配”。
  - 逃逸分析識別出 escapes to heap,并不一定就是堆分配,也可能是棧分配。
  - 進行內存分配器追蹤時,如果采集不到堆分配信息,那一定只有棧分配。
- 如何確定一個 Go 變量會被分配在哪里?
  - 先對代碼作逃逸分析。
  - 如果該變量被識別為 escapes to heap,那么它十有八九是被分配在堆上。
  - 如果該變量被識別為 does not escape,或者沒有與之相關的分析結果,那么它一定是被分配在棧上。
  - 如果對 escapes to heap 心存疑惑,就對代碼作內存分配器追蹤。
  - 如果有采集到與該變量相關的分配信息,那么它一定是被分配在堆上。否則,該變量一定是被分配在棧上。
  1. 怎么友好的關閉chan。

    • 只由 sender 來關閉

    • 一般不考慮關閉,除了一種情況:receiver 必須知道 sender 已經停止發送了

    如果有多個發送者,就用一個 sync.WaitGroup,每次增加發送者時 Add,發送者結束時 Done,最后在需要關閉的時候 Wait 完再 close。通知發送者結束可以用 context.Context.Done。

  2. 生產者消費型,用channel通信。

    package main
    
    import (
        "fmt"
    )
    
    var c = make(chan int, 50)
    var count = 0
    
    func main() {
        for i := 0; i < 5; i++ {
            go consumer(i)
        }
        for i := 0; i < 1000; i++ {
            c <- i
        }
        /** here **/
        fmt.Println(count)
    }
    
    func consumer(index int) {
        for target := range c {
            fmt.Printf("no.%d:%d\n", index, target)
            count++
        }
    }
    
    package main
    
    import (
        "fmt"
        "sync"
    )
    
    var c = make(chan int, 50)
    var count = 0
    var wg = new(sync.WaitGroup)
    
    func main() {
        for i := 0; i < 5; i++ {
            wg.Add(1)
            go consumer(i)
        }
        for i := 0; i < 1000; i++ {
            c <- i
        }
        wg.Wait()
        close(c)
        /** here **/
        fmt.Println(count)
    }
    
    func consumer(index int) {
        for target := range c {
            fmt.Printf("no.%d:%d\n", index, target)
            count++
            if len(c) <= 0 {
                wg.Done()
            }
         }
    }
    
  3. go實現協程池,用channel。

    package main
    
    import "fmt"
    import "time"
    
    //***************************Task部分
    
    //Task對外公開,所以要export
    type Task struct {
        f func() error  //一個task代表一個任務,函數形式
    }
    
    func NewTask(arg_f func() error) *Task {
        return &Task{
            f:arg_f,
        }
    }
    
    func (t *Task) Execute() {
        t.f()
    }
    
    //***************************Pool部分
    type Pool struct {
        EntryChan chan *Task  //對外的任務入口
        JobsChan chan *Task  //內部的任務隊列
        workerNum int  //協程池最大的worker數
    }
    
    func NewPool(num int) *Pool {
        return &Pool{
            EntryChan:make(chan *Task),
            JobsChan:make(chan *Task),
            workerNum:num,
        }
    }
    
    //Pool創建worker,由worker一直去JobsChan取任務并執行
    func (p *Pool) worker(workerId int) {
        for task := range p.JobsChan {
            task.Execute()
            fmt.Println("workerID ", workerId, " 執行完了一個任務")
        }
    }
    
    //Pool運行
    func (p *Pool) run() {
        //根據workerNum創建workers
        for i:=0;i<p.workerNum;i++ {
            go p.worker(i) //i作為workerId
        }
        //從EntryChan取任務,發送給JobsChan
        for task := range p.EntryChan {
            p.JobsChan <- task
        }
    }
    
    func main() {
        //創建任務
        t := NewTask(func() error {
            fmt.Println(time.Now())
        })
        //創建協程池
        p := NewPool(4)
        //把任務交給協程池
        go func() {
            for {
                p.EntryChan <- t
            }
        }
    
        //啟動協程池
        p.run()
    }
    
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容