05 雜七雜八但是重要

整理一些比較重要或 tricky 的 Go 語法或標準函式庫用法。

Tip: 到 Go Playground 寫點程式來測試和驗證自己的理解。

變數

宣告變數時,可使用關鍵字 var,並使用 = 運算子來賦值。

範例:

var x int
var y int = 100

沒有設定初始值的變數,都會有一個預設值。對 int 型別而言,這個預設值是 0,故此範例的 x 初始值為 0。

另一種更簡潔的語法是用 := 運算子來一次完成兩件事:宣告變數且賦值,而且不用寫 var。此寫法稱為 short declaration syntax。

範例:

sum := 100       // sum 是一個整數。
str := "hello"   // str 是一個字串。

型別轉換

Go 是靜態型別語言,編譯器會自動推測型別,也會判斷型別是否相容。指派變數值的時候,若來源型別和目的型別不相容,便需要手動轉型,否則編譯器會報錯。

範例:

var num int = 100

num = int64(50)   // 編譯錯誤。
num = 3.1416      // 編譯錯誤。
num = int(3.1416) // OK! num 的數值為 3。

取得型別資訊

這裡示範三種方法來取得變數的型別資訊:

  • 使用 fmt.Printf 的 %T 旗號。
  • 使用 reflect 套件。
  • 使用 type assertion。

使用 fmt.Printf 的 %T 旗號

var count int = 42
fmt.Printf("variable count=%v is of type %T \n", count, count)

使用 reflect 套件

使用 reflect.TypeOf() 方法:

fmt.Printf("%v", reflect.TypeOf(10))   // int
fmt.Printf("%v", reflect.TypeOf("Go")) // string

使用 type assertion

var x interface{} = 7

switch x.(type) {
case int:
    fmt.Println("int")
}

參閱 A Tour of Go: Type assertions

指標

Go 具備類似 C/C++ 的指標語法,但是更安全。Go 不允許指標運算,而且它有資源回收器在背後監視著每一個指標;當某一塊記憶體沒有任何指標指向它,Go 才會將那塊記憶體釋放。

Go 不允許指標運算,除非透過 unsafe 套件。參閱官方文件:https://pkg.go.dev/unsafe

宣告一個指標變數的語法是在型別前面加上星號 *

var p *int

這裡 p 是一個指向 int 的指標。由於沒有給初始值,故 p 的內容會是指標的預設值:nil

位址運算子

宣告為指標的變數,其內容就是一個記憶體位址,該位址所在的地方才是變數值所在的記憶體區塊。在操作指標時,除了 *,還會使用 & 符號:

  • 在變數名稱前面加上 & 符號會取得該變數所在的記憶體位址(address)。
  • 在指標變數名稱前面加上 * 符號則代表該指標所指向之變數的內容(value)。

範例:

num := 100   // 編譯器決定 num 是個 int。
ptr := &num  // 編譯器決定 ptr 是個指向變數 num 的指標。
*ptr = 200   // 把 ptr 指向的變數的內容改為 200。

fmt.Println(num)
fmt.Printf("Type of ptr: %T", ptr) // 印出 ptr 的型別名稱。

執行結果:

200
Type of ptr: *int

傳值還是傳址?

Go 只有傳值(pass by value)。也就是說,當我們把一個變數傳入某函式的參數時,該參數會是傳入之變數的新副本;在函式中修改那個參數值並不會改動先前的變數。

如果要讓函式可以修改傳入參數的變數內容,就要使用指標:

func main() {
    num := 100 // 編譯器決定 num 是個 int。

    increase(&num)
    fmt.Println(num) // 印出 101
}

func increase(n *int) {
    *n++
}

從函式回傳指標

Go 函式可以回傳一個指向函式區域變數的指標:

func newInt() *int {
    num := 42
    return &num
}

func main() {
    c := newInt()
    fmt.Println(*c)      // 印出 42
    fmt.Printf("%T", c)  // 印出 *int
}

newInt() 裡面的區域變數 num 所佔據的記憶體不會在函式返回之後立即消失,因為呼叫端 main() 函式有一個指標 c 仍指向 num 所在的記憶區塊。等到 num 所在的記憶區塊完全沒有人參考時,Go 的資源回收器便會將它所佔據的記憶體回收。

String

留意 Unicode 的字串長度:

unicodeCharStr := "地鼠"
fmt.Println(len(unicodeCharStr)) // output: 6

程式印出的結果是 6 而不是 2。這是因為 Go 的字串內部不是字元陣列,而是代表每個 UTF-8 字元的 byte 陣列。

因此,如果取出字串中的某個字元,不能以陣列索引的語法,否則結果不會是我們想要的:

unicodeCharStr := "地鼠"
for i := 0; i < len(unicodeCharStr); i++ {
    fmt.Print(string(unicodeCharStr[i]) + " ")
}
fmt.Println() // 輸出:  å  ° é ¼

要取出字串中的字元,可以用 range

unicodeCharStr := "地鼠"
for i, r := range unicodeCharStr {
    fmt.Printf("%d:%s ", i, string(r))
}
fmt.Println() // 輸出: 0:地 3:鼠

Blank identifier

呼叫函式時,如果某個回傳值無需處理,可以用一個 blank identifier 字元,也就是底線( _ )來承接該回傳值。

範例:

-, err = ReadFile("no/file)
if (err != nil) {
    fmt.Println("Error: err)
}

此範例所要表達的是:我不在乎 ReadFile() 執行成功時回傳的結果,而只看它是否返回錯誤。

init 函式

Go 有一個特殊用途的函式,名稱固定叫做 init()。此函式會在一個 package 載入時自動執行,故通常會把一些初始化的操作寫在此函式中。

特性與用法:

  • init 函式不能有參數和回傳值。
  • 每個 package 可以有多個 init 函式,而各 packages 的 init 函式的執行順序便是按照 packages 之間的依賴關係來決定。換言之,先載入哪個 package,就會先執行那個 package 的 init 函式。
  • 每個 .go 檔案也可以有多個 init 函式,這些函式會按照它們在檔案中出現的順序執行。但一般來說,通常不會在一個 .go 檔案裡面寫多個 init 函式,以免讓程式碼更難理解和維護。
  • 避免在 init 函式中執行耗時工作。

範例:

var greeting string

func init() {
    fmt.Println("Hello")
}

func main() {
    fmt.Println("world")
}

執行結果:

Hello
world

基本流程控制

for 迴圈

底下是幾種常見的寫法:

i := 1
for i <= 3 {
    fmt.Println(i)
}

for j := 0; j < 3; j++ {
    fmt.Println(j)
}

for {  // 無限迴圈
    fmt.Println("loop")
}

迴圈裡面可以用 continue 來進行下一圈,以及用 break 來跳離迴圈。

For-each range loop

使用 range 關鍵字來指定索引值的範圍:

for i := range 3 {  // i = 0, 1, 2
    fmt.Println("range", i)
}

常用來處理 arrays、slices、maps、channels 等結構:

strings := []string{"hello", "world"}
for i, s := range strings {
    fmt.Println(i, s)
}

執行結果:

0 hello
1 world

上例中,若不在乎陣列的索引值,可使用 blank identifier _ 取代 i

strings := []string{"hello", "world"}
for _, s := range strings {
    fmt.Println(s)
}

執行結果:

hello
world

If with a short statement

類似 for 迴圈,if 敘述也可以先有一個短敘述(short statement),然後才跟著判斷式。

範例:

func pow(x, n, lim float64) float64 {
    if v := math.Pow(x, n); v < lim {
        return v
    }
    return lim
}

第 2 行的意思是先把 math.Pow() 的結果指派給變數 v,然後判斷 v 是否小於 lim

注意:由 if 的短敘述所宣告的變數只活在那個 if 區塊內。

Defer 陳述句

Go 的 defer 關鍵字可用來將一個函式呼叫的執行時機延後至包覆函式(surrounding function)結束之前才執行,常用於清理資源(例如確保關閉資料庫連線)。

範例:

func main() {
    defer fmt.Println("World") // 離開 main 函式之前才執行此敘述。
    fmt.Println("Hello")
}

輸出結果:

Hello
World

清理資源

範例:

func doSomething() error {
  f, err := os.Open("test.txt")
  if err != nil {
    return err
  }
  defer f.Close()

  // 繼續處理檔案內容
}

注意:一旦檔案開啟成功,接著立刻加上 defer f.Close(),然後才處理後續的檔案操作,如此便可確保此函式離開之前會關閉檔案。

後進先出

如果在一個函式中使用了多次 defer,那些被延遲的函式呼叫將會以後進先出的順序執行。

範例:

func main() {
    defer fmt.Println(1)
    defer fmt.Println(2)
    defer fmt.Println(3)
}

輸出結果:

3
2
1

另外要注意的是,延後執行的時機除了函式正常返回,還有一種情況:goroutine 發生了執行時期的 panics。相關細節與注意事項可參閱官方文件:Defer statements


先這樣,也許有空時會再更新。   我的其他站點:      

Last modified: 2024-09-16