你真的熟Go嗎? Go in Action筆記整理 Q&A版-1

Ray Chen
9 min readFeb 21, 2023

--

【此文章非ChatGPT產生】,最近閱讀《Go in Action》一書的重點Part-1,針對已有Go開發經驗或好奇的人,分享了關於Go語言的重點知識,包括Slice、List、Map、Channel、Goroutine、物件設計模式、鴨子類型等。透過這些QA形式,可以更快速地了解Go的特性與使用方式,並且提升自己的Go技能水平或面試複習。

適用對象:

  1. 已經有寫過Go半年多,或做過一些Go application server。
  2. 已經有Java/Python等主流語言開發經驗,想了解Go特性。
  3. 適用想更瞭解Go的系統底層細節。
  4. Go面試複習。

不適用對象:

  1. 沒寫過Go的同學,建議可以到官網Tour學習。

Part-1 重點

  1. 關於go語言的介紹
  2. 數組(list)內部實現和基礎
  3. 切片(slice)內部實現和基礎
  4. 映射(map)內部實現和基礎

難度2/5顆星

Q: Channel的用途是什麼?

A: 作為goroutine同步傳遞資料的橋樑,且同一時間只有一個goroutine能Read/Write Channel內的資料,避免race condition。接著做為資料傳送橋梁,可達goroutine之間解隅的效果。

Q: Goroutine如何與thread合作?

A: 在啟動Go的process時,goroutine啟動時會映射到實際已運行的thread中,也可以分配多個goroutine至同一個thread中,因此goroutine可以高效與輕量的使用thread資源,不會因為過多goroutine導致效能下降或資源浪費。

Q: Go為什麼編譯快?

A: Go只關注被引用的library,所以import每次只引入單一功能,不像C++/Java要把所有依賴都掃描一次。

Q: Go是靜態語言還是動態語言?

A: Go是靜態語言,需要每次都compile。而動態語言不需要compile,所以很快,但是無法達成類型安全特性,不得用其他的套件測試,造成許多依賴。

Q: Go的物件設計模式是 繼承 還是 組合?

A: Go只有組合設計模式

小心! Go沒有繼承的概念

Q: Interface是怎麼implement的?

A: 不需要聲明Implement了哪個Interface,只需要instance(struct{})在implement時包含了interface的行為(行為就是那些function/var),稱作鴨子類型。

鴨子類型(Duck typing)是一種動態語言的特性,它的核心思想是“如果看起來像鴨子,叫起來像鴨子,那它就是一隻鴨子”,也就是說,當一個物件具備了特定的方法和屬性時,即使它不是該類型的物件,也可以被視為該類型的物件使用。

舉例來說,如果一個物件有一個名為”quack()”的方法,那麼即使它不是一隻真正的鴨子,也可以被視為一隻鴨子。這意味著,鴨子類型可以讓程式開發者更加靈活地編寫代碼,因為它不需要嚴格指定變量的類型,只需要關注物件是否有特定的方法和屬性即可。

鴨子類型通常用於動態語言中,如Python和Ruby等,但在靜態語言中也可以通過使用介面(Interface)實現類似的功能。

Q: 如果interface只包含一個func(method)和多個func(method)下,該怎麼命名?

A: 命名interface的時候,也需要遵守Go 語言的命名方式。如果interface只包含一個method,那麼這個類型的名字以 er 結尾。如果interface內部包含多個method,其interface名字需要與其行為關聯(不一定要er)。

Q: 給package命名時,要遵守什麼條件?

A: 簡潔、全小寫,例如net/http package下面的 cgi、httputil 和 pprof。

Q: 下面兩種寫法有什麼差別?

// 声明一个需要 8 MB 的数组,创建一个包含 100 万个 int 类型元素的数组
var array [1e6]int
// 将数组传递给函数 foo
foo(array)
// 函数 foo 接受一个 100 万个整型值的数组
func foo(array [1e6]int) {
...
}
// 分配一个需要 8 MB 的数组
var array [1e6]int
// 将数组的地址传递给函数 foo
foo(&array)
// 函数 foo 接受一个指向 100 万个整型值的数组的指针
func foo(array *[1e6]int) {
...
}

A: 下面寫法較優,呼叫foo時只會傳址(8 bytes);但上方寫法會傳遞array時會占用8MB的stack。

Q: Slice底層記憶體是怎麼配置的? 有什麼特點?

A: 配置在同一個連續區塊,所以才叫Slice(切片)。Slice能獲得Index, iteration, GC回收的優勢。

Q: 以下程式array1和array2的行為是?

// 声明第一个包含 5 个元素的字符串数组
var array1 [5]string
// 声明第二个包含 5 个元素的字符串数组
// 用颜色初始化数组
array2 := [5]string{“Red”, “Blue”, “Green”, “Yellow”, “Pink”}
// 把 array2 的值复制到 array1
array1 = array2

A: array1和array2是copy value。

Q: 以下程式array1和array2的行為是?

// 声明第一个包含 3 个元素的指向字符串的指针数组
var array1 [3]*string
// 声明第二个包含 3 个元素的指向字符串的指针数组
// 使用字符串指针初始化这个数组
array2 := [3]*string{new(string), new(string), new(string)}
// 使用颜色为每个元素赋值
*array2[0] = "Red"
*array2[1] = "Blue"
*array2[2] = "Green"
// 将 array2 复制给 array1
array1 = array2

A: array1和array2是copy reference

Q: Slice底層的數據結構是?

A: 包含 地址, 長度, 容量

slice := make([]int, 3, 5)
var slice []int

Q: newSlice與slice之間的關係是?

// 创建一个整型切片
// 其长度和容量都是 5 个元素
slice := []int{10, 20, 30, 40, 50}
// 创建一个新切片
// 其长度为 2 个元素,容量为 4 个元素
newSlice := slice[1:3]

A: newSlice[0]~newSlice[1]是指標到slice[1]~slice[2]

對底層數組容量是 k 的 slice[i:j]來說, 長度: j - i 容量: k - i

Q: 針對slice特性,以下會print出什麼?

package main

import (
"fmt"
)

func main() {
// 其长度和容量都是 5 个元素
slice := []int{10, 20, 30, 40, 50}
// 创建一个新切片
// 其长度为 2 个元素,容量为 4 个元素
newSlice := slice[1:3]
// 使用原有的容量来分配一个新元素
// 将新元素赋值为 60
newSlice = append(newSlice, 60)
fmt.Println(slice, newSlice)
}

A: [10 20 30 60 50] [20 30 60]

Q: 針對Slice特性,程式碼會如何把100萬個整數傳給foo?

// 分配包含 100 萬个整型值的slice
slice := make([]int, 1e6)
// 将 slice 传递到函数 foo
slice = foo(slice)
// 函数 foo 接收一个整型切片,并返回这个slice
func foo(slice []int) []int {
...
return slice
}

A: main會傳slice的24個bytes給foo, 8(slice address)+8(len data)+8(cap data),並不是傳slice那100萬個的底層整數,所以這是slice高效的原因之一

Q: Go的map是有序數列還是無序數列

A: 無序數列,因為使用類似hash key的search方式,在低位到高位時用hash key分類增加搜尋效率。故歷遍元素時,不會照著順序。

--

--

No responses yet