0%

Golang 學習記錄

前言

這篇文章主要來紀錄 Golang 的學習紀錄,使用的教材是碁峰出版的精通 Go 程式設計。

正文

程式結構

名稱

Go 的命名原則: 以 字母 or 底線 開頭

Go 有若干個關鍵字,只能用於語法中使用,因此他們不能拿來當作名稱!

break, default, func, interface, select, case, defer, go, map, struct, chan, else, goto, package, switch, const, fallthrough, if, range, type, continue, for, import, return, var

但是 Go 有一些 預先宣告 的名稱,是可以拿來重新宣告的!

常數: true false iota nil
型別: int uint float bool byte rune string error
函式: make len cap new append copy close delete complex real imag panic recover

名稱的第一個字母用來決定可見度是否可以跨越套件的邊界,如果名稱是以大寫字母開頭,在套件外是可見且可存取的,比方說 fmt 的 Print,套件的名稱本身永遠是小寫。

名稱長度無限制,但 Golang 的 convention 是習慣使用 短名稱 及 **駱駝式(Camel Case)**。比方說,變數 i 比 theLoopIndex 常見,或是習慣使用 aNewFunc 而不是 a_new_func

但這一切都是使用習慣,在業界大多是還是以部門的開發文化為主!

宣告

主要有四種宣告 var const type func,以下面的程式碼來做介紹

1
2
3
4
5
6
7
8
9
10
11
package main

import "fmt"

const boilingF = 212.0

func main() {
var f = boilingF
var c = (f - 32) * 5 / 9
fmt.Printf("boiling point = %gF or %gC\n", f, c)
}

boilingF 屬於套件層級的宣告,而 f, c 屬於區域宣告

變數

透過 var 來宣告特定型別的變數,格式為 var name type = expression,其中 型別 或是 = expression 兩者之一是可以省略的,但不能全部省略,如果省略型別,則會以 expression 來決定型別,如果省略 expression,則會以型別的零值來決定數值。

E.g.

數字 -> 0
布林值 > false
字串 > “”
interface & 參考型別 > nil
集合型別(array & struct) > 所有元素 or 欄位均為零值

零值的機制是為了確保變數有個正確定義的值,Go 沒有所謂的為定義變數,這麼做是為了不額外花心力的前提下確保合理的邊界條件。

1
2
var s string
fmt.Println(s) // ""

短變數宣告

以省略 var 的方式來宣告變數,格式為 name:=expression,其中 name 的型別是由 expression 來決定。

短變數宣告中,必須至少宣告一個新的變數

1
2
3
4
5
a, b := 1, 2

a, b := 2, 3 # Wrong -> no new variables on left side of :=

b, c := 3, 4 # Right

指標

指的是變數的位址,因此指標是所儲存值的位置。 並非每一個數值都會有位址,但 **每個變數都會有位址
**! 因此我們可以使用指標來讀取或是修改這些值

組合型別變數 - struct or array 的元素也都具有位址

任何型別的指標,其零值皆為 nil。指標也可以拿來互相比較,當兩個指標在同一個變數或均為 nil 才會相等。

1
2
3
4
5
var x, y int
fmt.Println(&x == &x, &x == &y, &x == nil, nil == nil) // true, false, false

# nil 不能拿來與自己比較
fmt.Println(nil == nil) # cannot compare nil == nil (operator == not defined for untyped nil)

在函式中可以安全的回傳區域變數的位址,對於 foo 內的區域變數 v,在呼叫完畢之後,還是能透過指標來指向他

1
2
3
4
5
6
7
8
func main() {
foo := func() *int {
v := 1
return &v
}

fmt.Println(foo() == foo()) # false
}
1
2
3
4
5
6
7
8
9
10
var n = flag.Bool("n", false, "omit trailing newline")
var sep = flag.String("s", " ", "seperator")

func main() {
flag.Parse()
fmt.Print(strings.Join(flag.Args(), *sep))
if !*n {
fmt.Println()
}
}

new 函式

透過內建的 new 來建構變數,建構一個 x型別的不具名變數**,也就是說 new(x) 是以 x 的零值去做初始化,並回傳 *x 型別的位址。

1
2
3
4
p := new(int)
fmt.Println(*p) // 0
*p = 3 // let p be 3
fmt.Println(*p) // 3

變數生命週期

指派

透過 指派 讓變數所保存的值進一步變化

1
2
3
4
x = 1  // 具名變數
*flag = false // 間接變數
person.name = "bob" // struct 欄位
count[x] = count[x] * scale // 陣列

資料組的指派

可以在同一行中去指派多個資料

1
2
x, y = y, x
a[i], a[j] = a[j], a[i]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func main() {
fmt.Println(gcd(484, 48)) // 22
fmt.Println(fib(10)) // 55
}

// 以 gcd 來做說明
func gcd(x, y int) int {
// golang 沒有 while,但可以用 `for ... `這種方式達成
for y != 0 {
x, y = y, x%y // 將 x,y 做處理後繼續找小公倍數
}
return x
}

# Fibonocci series
func fib(n int) int {
x, y := 0, 1
for i := 0; i < n; i++ {
x, y = y, x+y
}
return x
}

型別宣告

type 賦予固有的型別一個新的 具名型別,格式為 type name underlying-type,通常 name 會以大寫的方式呈現。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 以 float64 去定義新的型別 Celsius, Fahrenheit 
type Celsius float64
type Fahrenheit float64

const (
AbsoluteZeroC Celsius = -273.15
FreezingC Celsius = 0
BoilingC Celsius = 100
)

func main() {
fmt.Println(CTof(72))
}

func CTof(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) }

func FTof(f Fahrenheit) Celsius { return Celsius(f-32) * 5 / 9 }

基本資料型別

Go 的型別分成四種:

  1. 基本型別: 數字、字串、布林
  2. 集合型別: 陣列、Struct
  3. 參考型別: 指標、slice、map、函式、channel
  4. 介面型別

整數

可以分成 正負號 & 無正負號 & 四種大小 8、16、32、64 的組合。

要注意不同型別的變數不能做運算

1
2
3
var apples int32 = 1
var oranges int16 = 2
var compote int = apples + oranges // 編譯錯誤

複數

complex64 & complex128 這兩種。

1
2
3
4
5
6
var x complex128 = complex(1, 2)
var y complex128 = complex(3, 4)
x_y := x * y
fmt.Println(x_y) # (-5+10i)
fmt.Println((real(x_y))) # -5
fmt.Println(imag(x_y)) # 10

字串

嘗試存取範圍外的位元組會導致 panic

1
2
str := "a pine apple"
fmt.Println(str[len(str)])

可以透過 slice 來取得特定範圍的字串內容

1
2
3
4
str := "a pine apple"
fmt.Println(str[2:]) // pine apple
fmt.Println(str[:5]) // a pin
fmt.Println(str[:]) // a pine apple

常數

常數宣告可以指定 型別 與 值,在沒有明確指定型別時,型別由運算式的右手邊決定。

iota 常數產生器

const 宣告可使用 iota 常數產生器,這種型別也可以稱為列舉 Enum

1
2
3
4
5
6
7
8
9
10
11
12
13
type Weekday int
const (
Sunday Weekday = iota
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday
)

# 0, 1, 2, 3, 4, 5, 6
fmt.Println(Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday)
1
2
3
4
5
6
7
8
9
10
11
type Flags int
const (
FlagUp Flags = 1 << iota
FlagBroadcast
FlagLoopback
FlagPointToPoint
FlagMulticast
)

// 1 2 4 8 16
fmt.Println(FlagUp, FlagBroadcast, FlagLoopback, FlagPointToPoint, FlagMulticast)

組合型別

陣列

是由零到多個特定型別元素所組成的,因為它是固定長度,故陣列很少在 Go 中使用,反而可變大縮小的 slice 更有彈性。

陣列變數以元素型別的零值來做初始化

1
2
3
var arr1 [3]int = [3]int{1, 2, 3}
var arr2 [3]int = [3]int{1, 2}
fmt.Println(arr2[2])

也可以使用 ... 來代替長度

1
arr2 := [...]int{1, 2, 3}

陣列的長度大小也是用來判定型別是否相同的一部分因素,因此,[3]int[4]int 兩者是不同的型別。

1
2
arr1 := [3]int{1, 2, 3}
arr1 = [4]int{2, 3, 4, 5} // 不能將 [4]int 指派給 [3]int
1
2
3
4
5
6
7
8
9
建立 int 型別的新型別 Currency
const (
USD Currency = iota
EUR
GBP
RMB
)

symbol := [...]string{USB: "$", EUR: "€", GBP: "£", RMB: "¥"}

雜湊表 map

雜湊表是一個無排序的 key/value 群組,在這群組中的鍵值必定唯一,其表示方法為 map[K]V,其中 K & V 所分別代表的是鍵與值的型別。所有鍵 & 值 均為同一型別,但兩者不一定要同一型別。

Goroutine & Channel

每個並行執行的都被稱為 goroutine