1. 程式人生 > 其它 >Go常見面試題【由淺入深】2022版

Go常見面試題【由淺入深】2022版

Go語言相比C++/Java等語言是優雅且簡潔的,是筆者最喜愛的程式語言之一,它既保留了C++的高效能,又可以像Java,Python優雅的呼叫三方庫和管理專案,同時還有介面,自動垃圾回收和goroutine等讓人拍案叫絕的設計。

有許多基於Go的優秀專案。Docker,Kubernetes,etcd,deis,flynn,lime,revel等等。Go無疑是雲時代的最好語言!

題外話到此為止,在面試中,我們需要深入瞭解Go語言特性,並適當輔以原始碼閱讀(Go原始碼非常人性化,註釋非常詳細,基本上只要你學過Go就能看懂)來提升能力。常考的點包括:切片,通道,異常處理,Goroutine,GMP模型,字串高效拼接,指標,反射,介面,sync,go test和相關工具鏈。

一切問題的最權威的回答一定來自官方,這裡力薦golang官方FAQ,雖然是英文的,但是也希望你花3-4天看完。從使用者的角度去提問題, 從設計者的角度回答問題。

官方FAQ問題 https://golang.org/doc/faq​golang.org/doc/faq

面試題都是來源於網上和自己平時遇到的,但是很少有解答的版本,所以我專門回答了一下,放在專欄。

歡迎關注公眾號“跡寒程式設計”,回覆“go面試題”,獲取本文章的pdf版本。


【所有試題已註明來源,侵刪】


面試題1

來源:geektutu

基礎語法

01 = 和 := 的區別?

=是賦值變數,:=是定義變數。

02 指標的作用

一個指標可以指向任意變數的地址,它所指向的地址在32位或64位機器上分別固定佔4或8個位元組。指標的作用有:

  • 獲取變數的值
 import fmt
 ​
 func main(){
  a := 1
  p := &a//取址&
  fmt.Printf("%d\n", *p);//取值*
 }
  • 改變變數的值
 // 交換函式
 func swap(a, b *int) {
     *a, *b = *b, *a
 }
  • 用指標替代值傳入函式,比如類的接收器就是這樣的。
 type A struct{}
 ​
 func (a *A) fun(){}

 

03 Go 允許多個返回值嗎?

可以。通常函式除了一般返回值還會返回一個error。

 

04 Go 有異常型別嗎?

有。Go用error型別代替try...catch語句,這樣可以節省資源。同時增加程式碼可讀性:

 _, err := funcDemo()
if err != nil {
    fmt.Println(err)
    return
}

也可以用errors.New()來定義自己的異常。errors.Error()會返回異常的字串表示。只要實現error介面就可以定義自己的異常,

 type errorString struct {
  s string
 }
 ​
 func (e *errorString) Error() string {
  return e.s
 }
 ​
 // 多一個函式當作建構函式
 func New(text string) error {
  return &errorString{text}
 }

 

05 什麼是協程(Goroutine)

協程是使用者態輕量級執行緒,它是執行緒排程的基本單位。通常在函式前加上go關鍵字就能實現併發。一個Goroutine會以一個很小的棧啟動2KB或4KB,當遇到棧空間不足時,棧會自動伸縮, 因此可以輕易實現成千上萬個goroutine同時啟動。

 

06 ❤ 如何高效地拼接字串

拼接字串的方式有:+ , fmt.Sprintf , strings.Builderbytes.Bufferstrings.Join

1 "+"

使用+操作符進行拼接時,會對字串進行遍歷,計算並開闢一個新的空間來儲存原來的兩個字串。

 

2 fmt.Sprintf

由於採用了介面引數,必須要用反射獲取值,因此有效能損耗。

 

3 strings.Builder:

用WriteString()進行拼接,內部實現是指標+切片,同時String()返回拼接後的字串,它是直接把[]byte轉換為string,從而避免變數拷貝。

 

4 bytes.Buffer

bytes.Buffer是一個一個緩衝byte型別的緩衝器,這個緩衝器裡存放著都是byte

bytes.buffer底層也是一個[]byte切片。

 

5 strings.join

strings.join也是基於strings.builder來實現的,並且可以自定義分隔符,在join方法內呼叫了b.Grow(n)方法,這個是進行初步的容量分配,而前面計算的n的長度就是我們要拼接的slice的長度,因為我們傳入切片長度固定,所以提前進行容量分配可以減少記憶體分配,很高效。

效能比較:

strings.Join ≈ strings.Builder > bytes.Buffer > "+" > fmt.Sprintf

5種拼接方法的例項程式碼

func main(){
	a := []string{"a", "b", "c"}
	//方式1:+
	ret := a[0] + a[1] + a[2]
	//方式2:fmt.Sprintf
	ret := fmt.Sprintf("%s%s%s", a[0],a[1],a[2])
	//方式3:strings.Builder
	var sb strings.Builder
	sb.WriteString(a[0])
	sb.WriteString(a[1])
	sb.WriteString(a[2])
	ret := sb.String()
	//方式4:bytes.Buffer
	buf := new(bytes.Buffer)
	buf.Write(a[0])
	buf.Write(a[1])
	buf.Write(a[2])
	ret := buf.String()
	//方式5:strings.Join
	ret := strings.Join(a,"")
}

 

參考資料:字串拼接效能及原理 | Go 語言高效能程式設計 | 極客兔兔

07 什麼是 rune 型別

ASCII 碼只需要 7 bit 就可以完整地表示,但只能表示英文字母在內的128個字元,為了表示世界上大部分的文字系統,發明了 Unicode, 它是ASCII的超集,包含世界上書寫系統中存在的所有字元,併為每個程式碼分配一個標準編號(稱為Unicode CodePoint),在 Go 語言中稱之為 rune,是 int32 型別的別名。

Go 語言中,字串的底層表示是 byte (8 bit) 序列,而非 rune (32 bit) 序列。

sample := "我愛GO"
runeSamp := []rune(sample)
runeSamp[0] = '你'
fmt.Println(string(runeSamp))  // "你愛GO"
fmt.Println(len(runeSamp))  // 4

 

08 如何判斷 map 中是否包含某個 key ?

var sample map[int]int
if _, ok := sample[10]; ok {

} else {

}

 

09 Go 支援預設引數或可選引數嗎?

不支援。但是可以利用結構體引數,或者...傳入引數切片陣列。

// 這個函式可以傳入任意數量的整型引數
func sum(nums ...int) {
    total := 0
    for _, num := range nums {
        total += num
    }
    fmt.Println(total)
}

 

10 defer 的執行順序

defer執行順序和呼叫順序相反,類似於棧後進先出(LIFO)。

defer在return之後執行,但在函式退出之前,defer可以修改返回值。下面是一個例子:

func test() int {
	i := 0
	defer func() {
		fmt.Println("defer1")
	}()
	defer func() {
		i += 1
		fmt.Println("defer2")
	}()
	return i
}

func main() {
	fmt.Println("return", test())
}
// defer2
// defer1
// return 0

上面這個例子中,test返回值並沒有修改,這是由於Go的返回機制決定的,執行Return語句後,Go會建立一個臨時變數儲存返回值。如果是有名返回(也就是指明返回值func test() (i int)

func test() (i int) {
	i = 0
	defer func() {
		i += 1
		fmt.Println("defer2")
	}()
	return i
}

func main() {
	fmt.Println("return", test())
}
// defer2
// return 1

這個例子中,返回值被修改了。對於有名返回值的函式,執行 return 語句時,並不會再建立臨時變數儲存,因此,defer 語句修改了 i,即對返回值產生了影響。

 

11 如何交換 2 個變數的值?

對於變數而言a,b = b,a; 對於指標而言*a,*b = *b, *a

 

12 Go 語言 tag 的用處?

tag可以為結構體成員提供屬性。常見的:

  1. json序列化或反序列化時欄位的名稱
  2. db: sqlx模組中對應的資料庫欄位名
  3. form: gin框架中對應的前端的資料欄位名
  4. binding: 搭配 form 使用, 預設如果沒查詢到結構體中的某個欄位則不報錯值為空, binding為 required 代表沒找到返回錯誤給前端

 

13 如何獲取一個結構體的所有tag?

利用反射:

import reflect
type Author struct {
	Name         int      `json:Name`
	Publications []string `json:Publication,omitempty`
}

func main() {
	t := reflect.TypeOf(Author{})
	for i := 0; i < t.NumField(); i++ {
		name := t.Field(i).Name
		s, _ := t.FieldByName(name)
		fmt.Println(name, s.Tag)
	}
}

上述例子中,reflect.TypeOf方法獲取物件的型別,之後NumField()獲取結構體成員的數量。 通過Field(i)獲取第i個成員的名字。 再通過其Tag 方法獲得標籤。

 

14 如何判斷 2 個字串切片(slice) 是相等的?

reflect.DeepEqual() , 但反射非常影響效能。

 

15 結構體列印時,%v 和 %+v 的區別

%v輸出結構體各成員的值;

%+v輸出結構體各成員的名稱和值;

%#v輸出結構體名稱和結構體各成員的名稱和值

 

16 Go 語言中如何表示列舉值(enums)?

在常量中用iota可以表示列舉。iota從0開始。

const (
	B = 1 << (10 * iota)
	KiB 
	MiB
	GiB
	TiB
	PiB
	EiB
)

 

17 空 struct{} 的用途

  • 用map模擬一個set,那麼就要把值置為struct{},struct{}本身不佔任何空間,可以避免任何多餘的記憶體分配。
type Set map[string]struct{}

func main() {
	set := make(Set)

	for _, item := range []string{"A", "A", "B", "C"} {
		set[item] = struct{}{}
	}
	fmt.Println(len(set)) // 3
	if _, ok := set["A"]; ok {
		fmt.Println("A exists") // A exists
	}
}
  • 有時候給通道傳送一個空結構體,channel<-struct{}{},也是節省了空間。
func main() {
	ch := make(chan struct{}, 1)
	go func() {
		<-ch
		// do something
	}()
	ch <- struct{}{}
	// ...
}
  • 僅有方法的結構體
type Lamp struct{}

 

18 go裡面的int和int32是同一個概念嗎?

不是一個概念!千萬不能混淆。go語言中的int的大小是和作業系統位數相關的,如果是32位作業系統,int型別的大小就是4位元組。如果是64位作業系統,int型別的大小就是8個位元組。除此之外uint也與作業系統有關。

int8佔1個位元組,int16佔2個位元組,int32佔4個位元組,int64佔8個位元組。

 

實現原理

01 init() 函式是什麼時候執行的?

簡答: 在main函式之前執行。

詳細:init()函式是go初始化的一部分,由runtime初始化每個匯入的包,初始化不是按照從上到下的匯入順序,而是按照解析的依賴關係,沒有依賴的包最先初始化。

每個包首先初始化包作用域的常量和變數(常量優先於變數),然後執行包的init()函式。同一個包,甚至是同一個原始檔可以有多個init()函式。init()函式沒有入參和返回值,不能被其他函式呼叫,同一個包內多個init()函式的執行順序不作保證。

執行順序:import –> const –> var –>init()–>main()

一個檔案可以有多個init()函式!

02 ❤如何知道一個物件是分配在棧上還是堆上?

Go和C++不同,Go區域性變數會進行逃逸分析。如果變數離開作用域後沒有被引用,則優先分配到棧上,否則分配到堆上。那麼如何判斷是否發生了逃逸呢?

go build -gcflags '-m -m -l' xxx.go.

關於逃逸的可能情況:變數大小不確定,變數型別不確定,變數分配的記憶體超過使用者棧最大值,暴露給了外部指標。

 

03 2 個 interface 可以比較嗎 ?

Go 語言中,interface 的內部實現包含了 2 個欄位,型別 T 和 值 V,interface 可以使用 == 或 != 比較。2 個 interface 相等有以下 2 種情況

  1. 兩個 interface 均等於 nil(此時 V 和 T 都處於 unset 狀態)
  2. 型別 T 相同,且對應的值 V 相等。

看下面的例子:

type Stu struct {
     Name string
}

type StuInt interface{}

func main() {
     var stu1, stu2 StuInt = &Stu{"Tom"}, &Stu{"Tom"}
     var stu3, stu4 StuInt = Stu{"Tom"}, Stu{"Tom"}
     fmt.Println(stu1 == stu2) // false
     fmt.Println(stu3 == stu4) // true
}

stu1 和 stu2 對應的型別是 *Stu,值是 Stu 結構體的地址,兩個地址不同,因此結果為 false。
stu3 和 stu4 對應的型別是 Stu,值是 Stu 結構體,且各欄位相等,因此結果為 true。

04 2 個 nil 可能不相等嗎?

可能不等。interface在執行時繫結值,只有值為nil介面值才為nil,但是與指標的nil不相等。舉個例子:

var p *int = nil
var i interface{} = nil
if(p == i){
	fmt.Println("Equal")
}

兩者並不相同。總結:兩個nil只有在型別相同時才相等。

 

05 ❤簡述 Go 語言GC(垃圾回收)的工作原理

垃圾回收機制是Go一大特(nan)色(dian)。Go1.3採用標記清除法, Go1.5採用三色標記法,Go1.8採用三色標記法+混合寫屏障。

標記清除法

分為兩個階段:標記和清除

標記階段:從根物件出發尋找並標記所有存活的物件。

清除階段:遍歷堆中的物件,回收未標記的物件,並加入空閒連結串列。

缺點是需要暫停程式STW。

三色標記法

將物件標記為白色,灰色或黑色。

白色:不確定物件(預設色);黑色:存活物件。灰色:存活物件,子物件待處理。

標記開始時,先將所有物件加入白色集合(需要STW)。首先將根物件標記為灰色,然後將一個物件從灰色集合取出,遍歷其子物件,放入灰色集合。同時將取出的物件放入黑色集合,直到灰色集合為空。最後的白色集合物件就是需要清理的物件。

這種方法有一個缺陷,如果物件的引用被使用者修改了,那麼之前的標記就無效了。因此Go採用了寫屏障技術,當物件新增或者更新會將其著色為灰色。

一次完整的GC分為四個階段:

  1. 準備標記(需要STW),開啟寫屏障。
  2. 開始標記
  3. 標記結束(STW),關閉寫屏障
  4. 清理(併發)

基於插入寫屏障和刪除寫屏障在結束時需要STW來重新掃描棧,帶來效能瓶頸。混合寫屏障分為以下四步:

  1. GC開始時,將棧上的全部物件標記為黑色(不需要二次掃描,無需STW);
  2. GC期間,任何棧上建立的新物件均為黑色
  3. 被刪除引用的物件標記為灰色
  4. 被新增引用的物件標記為灰色

總而言之就是確保黑色物件不能引用白色物件,這個改進直接使得GC時間從 2s降低到2us。

06 函式返回區域性變數的指標是否安全?

這一點和C++不同,在Go裡面返回區域性變數的指標是安全的。因為Go會進行逃逸分析,如果發現區域性變數的作用域超過該函式則會把指標分配到堆區,避免記憶體洩漏。

 

07 非介面的任意型別 T() 都能夠呼叫 *T 的方法嗎?反過來呢?

一個T型別的值可以呼叫*T型別宣告的方法,當且僅當T是可定址的。

反之:*T 可以呼叫T()的方法,因為指標可以解引用。

 

08 go slice是怎麼擴容的?

Go <= 1.17

如果當前容量小於1024,則判斷所需容量是否大於原來容量2倍,如果大於,當前容量加上所需容量;否則當前容量乘2。

如果當前容量大於1024,則每次按照1.25倍速度遞增容量,也就是每次加上cap/4。

Go1.18之後,引入了新的擴容規則:淺談 Go 1.18.1的切片擴容機制

 

併發程式設計

01 ❤無緩衝的 channel 和有緩衝的 channel 的區別?

(這個問題筆者也糾結了很久,直到看到一篇文章,阻塞與否是分別針對傳送接收方而言的,才茅塞頓開)

對於無緩衝區channel:

傳送的資料如果沒有被接收方接收,那麼傳送方阻塞;如果一直接收不到傳送方的資料,接收方阻塞;

有緩衝的channel:

傳送方在緩衝區滿的時候阻塞,接收方不阻塞;接收方在緩衝區為空的時候阻塞,傳送方不阻塞。

可以類比生產者與消費者問題。

02 為什麼有協程洩露(Goroutine Leak)?

協程洩漏是指協程建立之後沒有得到釋放。主要原因有:

  1. 缺少接收器,導致傳送阻塞
  2. 缺少傳送器,導致接收阻塞
  3. 死鎖。多個協程由於競爭資源導致死鎖。
  4. 建立協程的沒有回收。

 

03 Go 可以限制執行時作業系統執行緒的數量嗎? 常見的goroutine操作函式有哪些?

可以,使用runtime.GOMAXPROCS(num int)可以設定執行緒數目。該值預設為CPU邏輯核數,如果設的太大,會引起頻繁的執行緒切換,降低效能。

runtime.Gosched(),用於讓出CPU時間片,讓出當前goroutine的執行許可權,排程器安排其它等待的任務執行,並在下次某個時候從該位置恢復執行。
runtime.Goexit(),呼叫此函式會立即使當前的goroutine的執行終止(終止協程),而其它的goroutine並不會受此影響。runtime.Goexit在終止當前goroutine前會先執行此goroutine的還未執行的defer語句。請注意千萬別在主函式呼叫runtime.Goexit,因為會引發panic。

04 如何控制協程數目。

The GOMAXPROCS variable limits the number of operating system threads that can execute user-level Go code simultaneously. There is no limit to the number of threads that can be blocked in system calls on behalf of Go code; those do not count against the GOMAXPROCS limit.

從官方文件的解釋可以看到,GOMAXPROCS 限制的是同時執行使用者態 Go 程式碼的作業系統執行緒的數量,但是對於被系統呼叫阻塞的執行緒數量是沒有限制的。GOMAXPROCS 的預設值等於 CPU 的邏輯核數,同一時間,一個核只能繫結一個執行緒,然後執行被排程的協程。因此對於 CPU 密集型的任務,若該值過大,例如設定為 CPU 邏輯核數的 2 倍,會增加執行緒切換的開銷,降低效能。對於 I/O 密集型應用,適當地調大該值,可以提高 I/O 吞吐率。

另外對於協程,可以用帶緩衝區的channel來控制,下面的例子是協程數為1024的例子

var wg sync.WaitGroup
ch := make(chan struct{}, 1024)
for i:=0; i<20000; i++{
	wg.Add(1)
	ch<-struct{}{}
	go func(){
		defer wg.Done()
		<-ch
	}
}
wg.Wait()

此外還可以用協程池:其原理無外乎是將上述程式碼中通道和協程函式解耦,並封裝成單獨的結構體。常見第三方協程池庫,比如tunny等。

面試題評價:★★★☆☆。偏容易和基礎。分為基礎語法、實現原理、併發程式設計三個大部分,需要讀者有紮實的基礎。

 


 

面試題2

來源:Durant Thorvalds

❤new和make的區別?

    • new只用於分配記憶體,返回一個指向地址的指標。它為每個新型別分配一片記憶體,初始化為0且返回型別*T的記憶體地址,它相當於&T{}
    • make只可用於slice,map,channel的初始化,返回的是引用。

 

請你講一下Go面向物件是如何實現的?

Go實現面向物件的兩個關鍵是struct和interface。

封裝:對於同一個包,物件對包內的檔案可見;對不同的包,需要將物件以大寫開頭才是可見的。

繼承:繼承是編譯時特徵,在struct內加入所需要繼承的類即可:

type A struct{}
type B struct{
A
}

多型:多型是執行時特徵,Go多型通過interface來實現。型別和介面是鬆耦合的,某個型別的例項可以賦給它所實現的任意介面型別的變數。

Go支援多重繼承,就是在型別中嵌入所有必要的父型別。

 

uint型變數值分別為 1,2,它們相減的結果是多少?

	var a uint = 1
	var b uint = 2
	fmt.Println(a - b)

答案,結果會溢位,如果是32位系統,結果是2^32-1,如果是64位系統,結果2^64-1.

 

講一下go有沒有函式在main之前執行?怎麼用?

go的init函式在main函式之前執行,它有如下特點:

func init() {
	...
}

init函式非常特殊:

  • 初始化不能採用初始化表示式初始化的變數;
  • 程式執行前執行註冊
  • 實現sync.Once功能
  • 不能被其它函式呼叫
  • init函式沒有入口引數和返回值:
  • 每個包可以有多個init函式,每個原始檔也可以有多個init函式。
  • 同一個包的init執行順序,golang沒有明確定義,程式設計時要注意程式不要依賴這個執行順序。
  • 不同包的init函式按照包匯入的依賴關係決定執行順序。

 

下面這句程式碼是什麼作用,為什麼要定義一個空值?

type GobCodec struct{
	conn io.ReadWriteCloser
	buf *bufio.Writer
	dec *gob.Decoder
	enc *gob.Encoder
}

type Codec interface {
	io.Closer
	ReadHeader(*Header) error
	ReadBody(interface{})  error
	Write(*Header, interface{}) error
}

var _ Codec = (*GobCodec)(nil)

答:將nil轉換為*GobCodec型別,然後再轉換為Codec介面,如果轉換失敗,說明*GobCodec沒有實現Codec介面的所有方法。

 

❤golang的記憶體管理的原理清楚嗎?簡述go記憶體管理機制。

golang記憶體管理基本是參考tcmalloc來進行的。go記憶體管理本質上是一個記憶體池,只不過內部做了很多優化:自動伸縮記憶體池大小,合理的切割記憶體塊。

一些基本概念:
頁Page:一塊8K大小的記憶體空間。Go向作業系統申請和釋放記憶體都是以頁為單位的。
span : 記憶體塊,一個或多個連續的 page 組成一個 span 。如果把 page 比喻成工人, span 可看成是小隊,工人被分成若干個隊伍,不同的隊伍幹不同的活。
sizeclass : 空間規格,每個 span 都帶有一個 sizeclass ,標記著該 span 中的 page 應該如何使用。使用上面的比喻,就是 sizeclass 標誌著 span 是一個什麼樣的隊伍。
object : 物件,用來儲存一個變數資料記憶體空間,一個 span 在初始化時,會被切割成一堆等大的 object 。假設 object 的大小是 16B , span 大小是 8K ,那麼就會把 span 中的 page 就會被初始化 8K / 16B = 512 個 object 。所謂記憶體分配,就是分配一個 object 出去。
  1. mheap

一開始go從作業系統索取一大塊記憶體作為記憶體池,並放在一個叫mheap的記憶體池進行管理,mheap將一整塊記憶體切割為不同的區域,並將一部分記憶體切割為合適的大小。

 

 

mheap.spans :用來儲存 page 和 span 資訊,比如一個 span 的起始地址是多少,有幾個 page,已使用了多大等等。

mheap.bitmap 儲存著各個 span 中物件的標記資訊,比如物件是否可回收等等。

mheap.arena_start : 將要分配給應用程式使用的空間。

  1. mcentral

用途相同的span會以連結串列的形式組織在一起存放在mcentral中。這裡用途用sizeclass來表示,就是該span儲存哪種大小的物件。

找到合適的 span 後,會從中取一個 object 返回給上層使用。

 

  1. mcache

為了提高記憶體併發申請效率,加入快取層mcache。每一個mcache和處理器P對應。Go申請記憶體首先從P的mcache中分配,如果沒有可用的span再從mcentral中獲取。

參考資料:Go 語言記憶體管理(二):Go 記憶體管理

 

❤mutex有幾種模式?

mutex有兩種模式:normal 和 starvation

正常模式

所有goroutine按照FIFO的順序進行鎖獲取,被喚醒的goroutine和新請求鎖的goroutine同時進行鎖獲取,通常新請求鎖的goroutine更容易獲取鎖(持續佔有cpu),被喚醒的goroutine則不容易獲取到鎖。公平性:否。

飢餓模式

所有嘗試獲取鎖的goroutine進行等待排隊,新請求鎖的goroutine不會進行鎖獲取(禁用自旋),而是加入佇列尾部等待獲取鎖。公平性:是。

參考連結:Go Mutex 飢餓模式,GO 互斥鎖(Mutex)原理

 


 

面試題3

來源:如果你是一個Golang面試官,你會問哪些問題?

❤go如何進行排程的。GMP中狀態流轉。

Go裡面GMP分別代表:G:goroutine,M:執行緒(真正在CPU上跑的),P:排程器。

 

GMP模型

排程器是M和G之間橋樑。

go進行排程過程:

  • 某個執行緒嘗試建立一個新的G,那麼這個G就會被安排到這個執行緒的G本地佇列LRQ中,如果LRQ滿了,就會分配到全域性佇列GRQ中;
  • 嘗試獲取當前執行緒的M,如果無法獲取,就會從空閒的M列表中找一個,如果空閒列表也沒有,那麼就建立一個M,然後繫結G與P執行。
  • 進入排程迴圈:
    • 找到一個合適的G
    • 執行G,完成以後退出

 

 

Go什麼時候發生阻塞?阻塞時,排程器會怎麼做。

  • 用於原子、互斥量或通道操作導致goroutine阻塞,排程器將把當前阻塞的goroutine從本地執行佇列LRQ換出,並重新排程其它goroutine;
  • 由於網路請求和IO導致的阻塞,Go提供了網路輪詢器(Netpoller)來處理,後臺用epoll等技術實現IO多路複用。

其它回答:

  • channel阻塞:當goroutine讀寫channel發生阻塞時,會呼叫gopark函式,該G脫離當前的M和P,排程器將新的G放入當前M。
  • 系統呼叫:當某個G由於系統呼叫陷入核心態,該P就會脫離當前M,此時P會更新自己的狀態為Psyscall,M與G相互繫結,進行系統呼叫。結束以後,若該P狀態還是Psyscall,則直接關聯該M和G,否則使用閒置的處理器處理該G。
  • 系統監控:當某個G在P上執行的時間超過10ms時候,或者P處於Psyscall狀態過長等情況就會呼叫retake函式,觸發新的排程。
  • 主動讓出:由於是協作式排程,該G會主動讓出當前的P(通過GoSched),更新狀態為Grunnable,該P會排程佇列中的G執行。
更多關於netpoller的內容可以參看:

❤Go中GMP有哪些狀態?

G的狀態:

_Gidle:剛剛被分配並且還沒有被初始化,值為0,為建立goroutine後的預設值

_Grunnable: 沒有執行程式碼,沒有棧的所有權,儲存在執行佇列中,可能在某個P的本地佇列或全域性佇列中(如上圖)。

_Grunning: 正在執行程式碼的goroutine,擁有棧的所有權(如上圖)。

_Gsyscall:正在執行系統呼叫,擁有棧的所有權,與P脫離,但是與某個M繫結,會在呼叫結束後被分配到執行佇列(如上圖)。

_Gwaiting:被阻塞的goroutine,阻塞在某個channel的傳送或者接收佇列(如上圖)。

_Gdead: 當前goroutine未被使用,沒有執行程式碼,可能有分配的棧,分佈在空閒列表gFree,可能是一個剛剛初始化的goroutine,也可能是執行了goexit退出的goroutine(如上圖)。

_Gcopystac:棧正在被拷貝,沒有執行程式碼,不在執行佇列上,執行權在

_Gscan : GC 正在掃描棧空間,沒有執行程式碼,可以與其他狀態同時存在。

P的狀態:

_Pidle :處理器沒有執行使用者程式碼或者排程器,被空閒佇列或者改變其狀態的結構持有,執行佇列為空

_Prunning :被執行緒 M 持有,並且正在執行使用者程式碼或者排程器(如上圖)

_Psyscall:沒有執行使用者程式碼,當前執行緒陷入系統呼叫(如上圖)

_Pgcstop :被執行緒 M 持有,當前處理器由於垃圾回收被停止

_Pdead :當前處理器已經不被使用

M的狀態:

自旋執行緒:處於執行狀態但是沒有可執行goroutine的執行緒,數量最多為GOMAXPROC,若是數量大於GOMAXPROC就會進入休眠。

非自旋執行緒:處於執行狀態有可執行goroutine的執行緒。

GMP能不能去掉P層?會怎麼樣?

P層的作用

  • 每個 P 有自己的本地佇列,大幅度的減輕了對全域性佇列的直接依賴,所帶來的效果就是鎖競爭的減少。而 GM 模型的效能開銷大頭就是鎖競爭。
  • 每個 P 相對的平衡上,在 GMP 模型中也實現了 Work Stealing 演算法,如果 P 的本地佇列為空,則會從全域性佇列或其他 P 的本地佇列中竊取可執行的 G 來執行,減少空轉,提高了資源利用率。

參考資料:

 

如果有一個G一直佔用資源怎麼辦?什麼是work stealing演算法?

如果有個goroutine一直佔用資源,那麼GMP模型會從正常模式轉變為飢餓模式(類似於mutex),允許其它goroutine使用work stealing搶佔(禁用自旋鎖)。

work stealing演算法指,一個執行緒如果處於空閒狀態,則幫其它正在忙的執行緒分擔壓力,從全域性佇列取一個G任務來執行,可以極大提高執行效率。

 

goroutine什麼情況會發生記憶體洩漏?如何避免。

在Go中記憶體洩露分為暫時性記憶體洩露和永久性記憶體洩露。

暫時性記憶體洩露

  • 獲取長字串中的一段導致長字串未釋放
  • 獲取長slice中的一段導致長slice未釋放
  • 在長slice新建slice導致洩漏

string相比切片少了一個容量的cap欄位,可以把string當成一個只讀的切片型別。獲取長string或者切片中的一段內容,由於新生成的物件和老的string或者切片共用一個記憶體空間,會導致老的string和切片資源暫時得不到釋放,造成短暫的記憶體洩漏

永久性記憶體洩露

  • goroutine永久阻塞而導致洩漏
  • time.Ticker未關閉導致洩漏
  • 不正確使用Finalizer(Go版本的解構函式)導致洩漏

 

Go GC有幾個階段

目前的go GC採用三色標記法和混合寫屏障技術。

Go GC有四個階段:

  • STW,開啟混合寫屏障,掃描棧物件;
  • 將所有物件加入白色集合,從根物件開始,將其放入灰色集合。每次從灰色集合取出一個物件標記為黑色,然後遍歷其子物件,標記為灰色,放入灰色集合;
  • 如此迴圈直到灰色集合為空。剩餘的白色物件就是需要清理的物件。
  • STW,關閉混合寫屏障;
  • 在後臺進行GC(併發)。

 

go競態條件瞭解嗎?

所謂競態競爭,就是當兩個或以上的goroutine訪問相同資源時候,對資源進行讀/寫。

比如var a int = 0,有兩個協程分別對a+=1,我們發現最後a不一定為2.這就是競態競爭。

通常我們可以用go run -race xx.go來進行檢測。

解決方法是,對臨界區資源上鎖,或者使用原子操作(atomics),原子操作的開銷小於上鎖。

 

如果若干個goroutine,有一個panic會怎麼做?

有一個panic,那麼剩餘goroutine也會退出,程式退出。如果不想程式退出,那麼必須通過呼叫 recover() 方法來捕獲 panic 並恢復將要崩掉的程式。

參考理解:goroutine配上panic會怎樣。

 

defer可以捕獲goroutine的子goroutine嗎?

不可以。它們處於不同的排程器P中。對於子goroutine,必須通過 recover() 機制來進行恢復,然後結合日誌進行列印(或者通過channel傳遞error),下面是一個例子:

// 心跳函式
func Ping(ctx context.Context) error {
    ... code ...
 
	go func() {
		defer func() {
			if r := recover(); r != nil {
				log.Errorc(ctx, "ping panic: %v, stack: %v", r, string(debug.Stack()))
			}
		}()
 
        ... code ...
	}()
 
    ... code ...
 
	return nil
}

 

 

❤gRPC是什麼?

基於go的遠端過程呼叫。RPC 框架的目標就是讓遠端服務呼叫更加簡單、透明,RPC 框架負責遮蔽底層的傳輸方式(TCP 或者 UDP)、序列化方式(XML/Json/ 二進位制)和通訊細節。服務呼叫者可以像呼叫本地介面一樣呼叫遠端的服務提供者,而不需要關心底層通訊細節和呼叫過程。

gRPC框架圖

 

面試題4

需要面試者有一定的大型專案經驗經驗,瞭解使用微服務,etcd,gin,gorm,gRPC等典型框架等模型或框架。

微服務瞭解嗎?

微服務是一種開發軟體的架構和組織方法,其中軟體由通過明確定義的 API 進行通訊的小型獨立服務組成。微服務架構使應用程式更易於擴充套件和更快地開發,從而加速創新並縮短新功能的上市時間。

微服務示意圖

微服務有著自主,專用,靈活性等優點。

參考資料:什麼是微服務?| AWS

 

服務發現是怎麼做的?

主要有兩種服務發現機制:客戶端發現和服務端發現。

客戶端發現模式:當我們使用客戶端發現的時候,客戶端負責決定可用服務例項的網路地址並且在叢集中對請求負載均衡, 客戶端訪問服務登記表,也就是一個可用服務的資料庫,然後客戶端使用一種負載均衡演算法選擇一個可用的服務例項然後發起請求。該模式如下圖所示:

客戶端發現模式

服務端發現模式:客戶端通過負載均衡器向某個服務提出請求,負載均衡器查詢服務登錄檔,並將請求轉發到可用的服務例項。如同客戶端發現,服務例項在服務登錄檔中註冊或登出。

服務端發現模式

 

參考資料:「Chris Richardson 微服務系列」服務發現的可行方案以及實踐案例

ETCD用過嗎?

etcd是一個高度一致的分散式鍵值儲存,它提供了一種可靠的方式來儲存需要由分散式系統或機器叢集訪問的資料。它可以優雅地處理網路分割槽期間的領導者選舉,即使在領導者節點中也可以容忍機器故障。

etcd 是用Go語言編寫的,它具有出色的跨平臺支援,小的二進位制檔案和強大的社群。etcd機器之間的通訊通過Raft共識演算法處理。

關於文件可以參考:v3.5 docs

GIN怎麼做引數校驗?

go採用validator作引數校驗。

它具有以下獨特功能:

  • 使用驗證tag或自定義validator進行跨欄位Field和跨結構體驗證。
  • 允許切片、陣列和雜湊表,多維欄位的任何或所有級別進行校驗。
  • 能夠對雜湊表key和value進行驗證
  • 通過在驗證之前確定它的基礎型別來處理型別介面。
  • 別名驗證標籤,允許將多個驗證對映到單個標籤,以便更輕鬆地定義結構體上的驗證
  • gin web 框架的預設驗證器;

參考資料:validator package - pkg.go.dev

中介軟體用過嗎?

Middleware是Web的重要組成部分,中介軟體(通常)是一小段程式碼,它們接受一個請求,對其進行處理,每個中介軟體只處理一件事情,完成後將其傳遞給另一箇中間件或最終處理程式,這樣就做到了程式的解耦。

Go解析Tag是怎麼實現的?

Go解析tag採用的是反射。

具體來說使用reflect.ValueOf方法獲取其反射值,然後獲取其Type屬性,之後再通過Field(i)獲取第i+1個field,再.Tag獲得Tag。

反射實現的原理在: `src/reflect/type.go`中

你專案有優雅的啟停嗎?

所謂「優雅」啟停就是在啟動退出服務時要滿足以下幾個條件:

  • 不可以關閉現有連線(程序)
  • 新的程序啟動並「接管」舊程序
  • 連線要隨時響應使用者請求,不可以出現拒絕請求的情況
  • 停止的時候,必須處理完既有連線,並且停止接收新的連線。

為此我們必須引用訊號來完成這些目的:

啟動:

  • 監聽SIGHUP(在使用者終端連線(正常或非正常)結束時發出);
  • 收到訊號後將服務監聽的檔案描述符傳遞給新的子程序,此時新老程序同時接收請求;

退出:

  • 監聽SIGINT和SIGSTP和SIGQUIT等。
  • 父程序停止接收新請求,等待舊請求完成(或超時);
  • 父程序退出。

實現:go1.8採用Http.Server內建的Shutdown方法支援優雅關機。 然後fvbock/endless可以實現優雅重啟。

參考資料:gin框架實踐連載八 | 如何優雅重啟和停止 - 掘金,優雅地關閉或重啟 go web 專案

 

持久化怎麼做的?

所謂持久化就是將要儲存的字串寫到硬碟等裝置。

  • 最簡單的方式就是採用ioutil的WriteFile()方法將字串寫到磁碟上,這種方法面臨格式化方面的問題。
  • 更好的做法是將資料按照固定協議進行組織再進行讀寫,比如JSON,XML,Gob,csv等。
  • 如果要考慮高併發和高可用,必須把資料放入到資料庫中,比如MySQL,PostgreDB,MongoDB等。

參考連結:Golang 持久化


面試題5

作者:Dylan2333 連結:

測開轉Go開發-面經&總結_筆經面經_牛客網​www.nowcoder.com/discuss/826193?type=post&order=recall&pos=&page=1&ncTraceId=&channel=-1&source_id=search_post_nctrack&gio_id=9C5DC1FFB3FC3BE29281D7CCFC420365-1645173894793

 

該試題需要面試者有非常豐富的專案閱歷和底層原理經驗,熟練使用微服務,etcd,gin,gorm,gRPC等典型框架等模型或框架。

channel 死鎖的場景

  • 當一個channel中沒有資料,而直接讀取時,會發生死鎖:
q := make(chan int,2)
<-q

解決方案是採用select語句,再default放預設處理方式:

q := make(chan int,2)
select{
   case val:=<-q:
   default:
         ...

}
  • 當channel資料滿了,再嘗試寫資料會造成死鎖:
q := make(chan int,2)
q<-1
q<-2
q<-3

解決方法,採用select

func main() {
	q := make(chan int, 2)
	q <- 1
	q <- 2
	select {
	case q <- 3:
		fmt.Println("ok")
	default:
		fmt.Println("wrong")
	}

}
  • 向一個關閉的channel寫資料。

注意:一個已經關閉的channel,只能讀資料,不能寫資料。

參考資料:Golang關於channel死鎖情況的彙總以及解決方案

 

對已經關閉的chan進行讀寫會怎麼樣?

  • 讀已經關閉的chan能一直讀到東西,但是讀到的內容根據通道內關閉前是否有元素而不同。
    • 如果chan關閉前,buffer內有元素還未讀,會正確讀到chan內的值,且返回的第二個bool值(是否讀成功)為true。
    • 如果chan關閉前,buffer內有元素已經被讀完,chan內無值,接下來所有接收的值都會非阻塞直接成功,返回 channel 元素的零值,但是第二個bool值一直為false。

寫已經關閉的chan會panic。

 

說說 atomic底層怎麼實現的.

atomic原始碼位於`sync\atomic`。通過閱讀原始碼可知,atomic採用CAS(CompareAndSwap)的方式實現的。所謂CAS就是使用了CPU中的原子性操作。在操作共享變數的時候,CAS不需要對其進行加鎖,而是通過類似於樂觀鎖的方式進行檢測,總是假設被操作的值未曾改變(即與舊值相等),並一旦確認這個假設的真實性就立即進行值替換。本質上是不斷佔用CPU資源來避免加鎖的開銷。

參考資料:Go語言的原子操作atomic - 程式設計獵人

 

channel底層實現?是否執行緒安全。

channel底層實現在src/runtime/chan.go

channel內部是一個迴圈連結串列。內部包含buf, sendx, recvx, lock ,recvq, sendq幾個部分;

buf是有緩衝的channel所特有的結構,用來儲存快取資料。是個迴圈連結串列;

  • sendx和recvx用於記錄buf這個迴圈連結串列中的傳送或者接收的index;
  • lock是個互斥鎖;
  • recvq和sendq分別是接收(<-channel)或者傳送(channel <- xxx)的goroutine抽象出來的結構體(sudog)的佇列。是個雙向連結串列。

channel是執行緒安全的。

參考資料:Kitou:Golang 深度剖析 -- channel的底層實現

 

map的底層實現。

原始碼位於src\runtime\map.go 中。

go的map和C++map不一樣,底層實現是雜湊表,包括兩個部分:hmap和bucket。

裡面最重要的是buckets(桶),buckets是一個指標,最終它指向的是一個結構體:

// A bucket for a Go map.
type bmap struct {
    tophash [bucketCnt]uint8
}

每個bucket固定包含8個key和value(可以檢視原始碼bucketCnt=8).實現上面是一個固定的大小連續記憶體塊,分成四部分:每個條目的狀態,8個key值,8個value值,指向下個bucket的指標。

建立雜湊表使用的是makemap函式.map 的一個關鍵點在於,雜湊函式的選擇。在程式啟動時,會檢測 cpu 是否支援 aes,如果支援,則使用 aes hash,否則使用 memhash。這是在函式 alginit() 中完成,位於路徑:src/runtime/alg.go 下。

map查詢就是將key雜湊後得到64位(64位機)用最後B個位元位計算在哪個桶。在 bucket 中,從前往後找到第一個空位。這樣,在查詢某個 key 時,先找到對應的桶,再去遍歷 bucket 中的 key。

關於map的查詢和擴容可以參考map的用法到map底層實現分析。

 

 

select的實現原理?

select原始碼位於src\runtime\select.go,最重要的scase 資料結構為:

type scase struct {
	c    *hchan         // chan
	elem unsafe.Pointer // data element
}

scase.c為當前case語句所操作的channel指標,這也說明了一個case語句只能操作一個channel。

scase.elem表示緩衝區地址:

  • caseRecv : scase.elem表示讀出channel的資料存放地址;
  • caseSend : scase.elem表示將要寫入channel的資料存放地址;

select的主要實現位於:select.go函式:其主要功能如下:

1. 鎖定scase語句中所有的channel

2. 按照隨機順序檢測scase中的channel是否ready

2.1 如果case可讀,則讀取channel中資料,解鎖所有的channel,然後返回(case index, true)

2.2 如果case可寫,則將資料寫入channel,解鎖所有的channel,然後返回(case index, false)

2.3 所有case都未ready,則解鎖所有的channel,然後返回(default index, false)

3. 所有case都未ready,且沒有default語句

3.1 將當前協程加入到所有channel的等待佇列

3.2 當將協程轉入阻塞,等待被喚醒

4. 喚醒後返回channel對應的case index

4.1 如果是讀操作,解鎖所有的channel,然後返回(case index, true)

4.2 如果是寫操作,解鎖所有的channel,然後返回(case index, false)

參考資料:Go select的使用和實現原理.

 

go的interface怎麼實現的?

go interface原始碼在runtime\iface.go中。

go的介面由兩種型別實現ifaceeface。iface是包含方法的介面,而eface不包含方法。

  • iface

對應的資料結構是(位於src\runtime\runtime2.go):

type iface struct {
	tab  *itab
	data unsafe.Pointer
}

可以簡單理解為,tab表示介面的具體結構型別,而data是介面的值。

  • itab:
type itab struct {
	inter *interfacetype //此屬性用於定位到具體interface
	_type *_type //此屬性用於定位到具體interface
	hash  uint32 // copy of _type.hash. Used for type switches.
	_     [4]byte
	fun   [1]uintptr // variable sized. fun[0]==0 means _type does not implement inter.
}

屬性interfacetype類似於_type,其作用就是interface的公共描述,類似的還有maptypearraytypechantype…其都是各個結構的公共描述,可以理解為一種外在的表現資訊。interfaetype和type唯一確定了介面型別,而hash用於查詢和型別判斷。fun表示方法集。

  • eface

與iface基本一致,但是用_type直接表示型別,這樣的話就無法使用方法。

type eface struct {
	_type *_type
	data  unsafe.Pointer
}

這裡篇幅有限,深入討論可以看:深入研究 Go interface 底層實現

 

go的reflect 底層實現

go reflect原始碼位於src\reflect\下面,作為一個庫獨立存在。反射是基於介面實現的。

Go反射有三大法則:

  • 反射從介面對映到反射物件;
法則1
  • 反射從反射物件對映到介面值;
法則2
  • 只有值可以修改(settable),才可以修改反射物件。

Go反射基於上述三點實現。我們先從最核心的兩個原始檔入手type.govalue.go.

type用於獲取當前值的型別。value用於獲取當前的值。

參考資料:The Laws of Reflection, 圖解go反射實現原理

 

go GC的原理知道嗎?

如果需要從原始碼角度解釋GC,推薦閱讀(非常詳細,圖文並茂):

 

go裡用過哪些設計模式 ?

Go設計模式常見面試題【2022版】8 贊同 · 4 評論文章

 

go的除錯/分析工具用過哪些。

go的自帶工具鏈相當豐富,

  • go cover : 測試程式碼覆蓋率;
  • godoc: 用於生成go文件;
  • pprof:用於效能調優,針對cpu,記憶體和併發;
  • race:用於競爭檢測;

 

程序被kill,如何保證所有goroutine順利退出

goroutine監聽SIGKILL訊號,一旦接收到SIGKILL,則立刻退出。可採用select方法。

var wg = &sync.WaitGroup{}

func main() {
	wg.Add(1)

	go func() {
		c1 := make(chan os.Signal, 1)
		signal.Notify(c1, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
		fmt.Printf("goroutine 1 receive a signal : %v\n\n", <-c1)
		wg.Done()
	}()

	wg.Wait()
	fmt.Printf("all groutine done!\n")
}

 

說說context包的作用?你用過哪些,原理知道嗎?

context可以用來在goroutine之間傳遞上下文資訊,相同的context可以傳遞給執行在不同goroutine中的函式,上下文對於多個goroutine同時使用是安全的,context包定義了上下文型別,可以使用backgroundTODO建立一個上下文,在函式呼叫鏈之間傳播context,也可以使用WithDeadlineWithTimeoutWithCancel 或 WithValue 建立的修改副本替換它,聽起來有點繞,其實總結起就是一句話:context的作用就是在不同的goroutine之間同步請求特定的資料、取消訊號以及處理請求的截止日期。

關於context原理,可以參看:小白也能看懂的context包詳解:從入門到精通

 

grpc為啥好,基本原理是什麼,和http比呢

官方介紹:gRPC 是一個現代開源的高效能遠端過程呼叫 (RPC) 框架,可以在任何環境中執行。它可以通過對負載平衡、跟蹤、健康檢查和身份驗證的可插拔支援有效地連線資料中心內和跨資料中心的服務。它也適用於分散式計算的最後一英里,將裝置、移動應用程式和瀏覽器連線到後端服務。

區別:
- rpc是遠端過程呼叫,就是本地去呼叫一個遠端的函式,而http是通過 url和符合restful風格的資料包去傳送和獲取資料;
- rpc的一般使用的編解碼協議更加高效,比如grpc使用protobuf編解碼。而http的一般使用json進行編解碼,資料相比rpc更加直觀,但是資料包也更大,效率低下;
- rpc一般用在服務內部的相互呼叫,而http則用於和使用者互動;
相似點:
都有類似的機制,例如grpc的metadata機制和http的頭機制作用相似,而且web框架,和rpc框架中都有攔截器的概念。grpc使用的是http2.0協議。
官網:gRPC

etcd怎麼搭建的,具體怎麼用的

熔斷怎麼做的

服務降級怎麼搞

1億條資料動態增長,取top10,怎麼實現

程序掛了怎麼辦

nginx配置過嗎,有哪些注意的點

設計一個阻塞佇列

mq消費阻塞怎麼辦

效能沒達到預期,有什麼解決方案

 


 

程式設計系列

實現使用字串函式名,呼叫函式。

思路:採用反射的Call方法實現。

package main
import (
	"fmt"
    "reflect"
)

type Animal struct{
    
}

func (a *Animal) Eat(){
    fmt.Println("Eat")
}

func main(){
    a := Animal{}
    reflect.ValueOf(&a).MethodByName("Eat").Call([]reflect.Value{})
    
}

 

(Goroutine)有三個函式,分別列印"cat", "fish","dog"要求每一個函式都用一個goroutine,按照順序列印100次。

此題目考察channel,用三個無緩衝channel,如果一個channel收到訊號則通知下一個。

package main

import (
	"fmt"
	"time"
)

var dog = make(chan struct{})
var cat = make(chan struct{})
var fish = make(chan struct{})

func Dog() {
	<-fish
	fmt.Println("dog")
	dog <- struct{}{}
}

func Cat() {
	<-dog
	fmt.Println("cat")
	cat <- struct{}{}
}

func Fish() {
	<-cat
	fmt.Println("fish")
	fish <- struct{}{}
}

func main() {
	for i := 0; i < 100; i++ {
		go Dog()
		go Cat()
		go Fish()
	}
	fish <- struct{}{}

	time.Sleep(10 * time.Second)
}

 

兩個協程交替列印10個字母和數字

思路:採用channel來協調goroutine之間順序。

主執行緒一般要waitGroup等待協程退出,這裡簡化了一下直接sleep。

package main

import (
	"fmt"
	"time"
)

var word = make(chan struct{}, 1)
var num = make(chan struct{}, 1)

func printNums() {
	for i := 0; i < 10; i++ {
		<-word
		fmt.Println(1)
		num <- struct{}{}
	}
}
func printWords() {
	for i := 0; i < 10; i++ {
		<-num
		fmt.Println("a")
		word <- struct{}{}
	}
}

func main() {
	num <- struct{}{}
	go printNums()
	go printWords()
	time.Sleep(time.Second * 1)
}

程式碼: 

@中二的灰太狼

 

啟動 2個groutine 2秒後取消, 第一個協程1秒執行完,第二個協程3秒執行完。

思路:採用ctx, _ := context.WithTimeout(context.Background(), time.Second*2)實現2s取消。協程執行完後通過channel通知,是否超時。

package main

import (
	"context"
	"fmt"
	"time"
)

func f1(in chan struct{}) {

	time.Sleep(1 * time.Second)
	in <- struct{}{}

}

func f2(in chan struct{}) {
	time.Sleep(3 * time.Second)
	in <- struct{}{}
}

func main() {
	ch1 := make(chan struct{})
	ch2 := make(chan struct{})
	ctx, _ := context.WithTimeout(context.Background(), 2*time.Second)

	go func() {
		go f1(ch1)
		select {
		case <-ctx.Done():
			fmt.Println("f1 timeout")
			break
		case <-ch1:
			fmt.Println("f1 done")
		}
	}()

	go func() {
		go f2(ch2)
		select {
		case <-ctx.Done():
			fmt.Println("f2 timeout")
			break
		case <-ch2:
			fmt.Println("f2 done")
		}
	}()
	time.Sleep(time.Second * 5)
}

程式碼: 

@中二的灰太狼

 

當select監控多個chan同時到達就緒態時,如何先執行某個任務?

可以在子case再加一個for select語句。

func priority_select(ch1, ch2 <-chan string) {
	for {
		select {
		case val := <-ch1:
			fmt.Println(val)
		case val2 := <-ch2:
		priority:
			for {
				select {
				case val1 := <-ch1:
					fmt.Println(val1)

				default:
					break priority
				}
			}
			fmt.Println(val2)
		}
	}

}

 

總結

Go面試複習應該有所側重,關注切片,通道,異常處理,Goroutine,GMP模型,字串高效拼接,指標,反射,介面,sync。對於比較難懂的部分,GMP模型和GC和記憶體管理,應該主動去看原始碼,然後慢慢理解。業務程式碼寫多了,自然就有理解了。

推薦部落格:

煎魚​eddycjy.com/ Go 語言設計與實現​draveness.me/golang/

圖書:

《Go語言底層原理剖析》

《Go高效能程式設計》