基礎
包
package 是 go 程序的組成單元。
所有程序都以名爲 main
的包爲入口。
包的名字定義在文件起始處。
變量
標準變量聲明和賦值爲
var x int
x = 1
同時聲明多個同類型變量
var x, y int
聲明 & 初始化
var x int = 1
因爲初始化可以推導類型,所以類型聲明可以省略
var x = 1
語法上進一步簡化,但僅限在函數內部使用
x := 1
常量
const SUN = "日"
top level declaration
... int x ... { // 這種聲明的變量 x 作用域僅限花括號內
}
函數
定義:
// 單返回值
func Add(x, y int) int {
return x + y
}
// 多返回值
func Swap(a, b string) (string, string) {
return b, a
}
a, b := swap("hello", "world)
一種奇怪的定義返回值的方式:
func foo() (x int) { // x 的變量聲明在 top level declaration 處
x = 1 // 函數內只需給 x 賦值即可
return // “裸返回”,即返回函數聲明處的變量
}
// 裸返回還有一種應用形式是函數沒有返回值
這種方式的用處是通過變量名來做 doc 說明。因爲看起來比較亂所以只應用在非常短的函數上。
類型
基礎類型有
bool
string
int
byte (alias for uint8)
rune (alias for uint32, reepresents for a unicode code point)
float
complex
具體參考官方文檔。
未初始化的變量會有一個 “零值(Zero value)”,默認爲 0
, false
,""
或 nil
。
類型轉換表達式: T(v)
操作符
流程控制
for
for i:=0; i<10; i++ {
fmt.Println(i)
}
外部變量可省略聲明
i := 0
for ; i<10; i++{
}
甚至 i++
也可以省。。。
i := 0
for ; i<10; {
}
while
沒有 while,用 for 實現
forever
for {
break
}
if
if (x<1) {
} else {
}
()
允許省略.
像其他頂層聲明一樣,if 裏面也可以聲明一個局部變量
MinAge = 10
if (tomsAge := GetAge(tom); tomsAge < MinAge) {
return tomsAge
}
return MinAge
switch
同上, switch 也可以定義局部變量
switch os := runtime.GOOS; os {
case "windows":
...
fallthrough
case "darwin":
...
default:
...
}
switch
在命中 case
時默認會 break
,避免這種行爲需要使用 fallthrough
表達式.
switch 可以不跟 condition,這時等價於 switch true
。這種用法可以取代很長的 if... else...
switch {
case x < 10:
...
case x < 100:
...
default:
...
}
defer
一種別緻函數調用方式,被 defer 的函數會立即計算參數表達式,但函數執行則在宿主函數 return 之後。有點類似 Python 的上下文管理器。
func handle_request int {
defer connection.close()
...
}
類型
指針
var p *int
i := 1
p = &i
*p = 2
go 沒有指針運算,指針的作用可能僅限於作爲一種引用。
結構
type Point struct {
X int
Y int
}
p := Point{1, 2}
p.x = 3
結構指針的成員訪問標準方法爲 (*p).X
, 但因爲 go 的指針較簡單,所以提供了一個簡化語法:p.X
。
結構初始化時可以只部分賦值,未賦值成員爲零值:
var (
p1 = Point{1, 2} # 全初始化
p2 = Point{x: 1} # 偏初始化,y=0
p3 = Point{} # x=0, y=0
)
數組 & 切片
聲明方式爲 [n]T
, 如
var a [10]int
a := [3]int{1, 2, 3}
切片語法與 Python 相同,即支持以下語法:
a[0]
a[1:5]
a[1:]
a[:5]
a[:]
切片聲明爲 var s []int
,與數組的區別僅在沒有指定長度,字面量語法爲:
s := []int{1, 2, 3}
這個表達式等於創建了一個長度爲 3 的匿名數組然後切片之。
數組切片有兩個屬性:長度(length)和容量(capacity)。分別爲切片的元素個數,和其對應數組從 s[0] 開始剩餘元素的個數。可以分別通過 len(s)
和 cap(s)
訪問。這個設計更加體現了切片是數組的一個投影這種描述,並且使切片變得可動態擴展了:
s := []int{1, 2, 3, 4, 5} // 長度和容量初始都爲 5
s = s[0:] // 將切片的長度設爲 0,但容量不變
s1 = s[:3] // 長度可重新擴展到 3
s2 = s1[1:] // 長度和容量都調整爲 4,此時數組的第一個元素永遠消失了,猜測有可能會被 gc 回收
cap 賦予切片比較有趣的屬性便是可以對切片進行超過其長度的切片,只要在容量範圍內即可。即對切片進行切片其實切的是其容量而非長度。但元素訪問(s[i]
)是對長度的訪問,超出長度會導致 panic: runtime error: index out of range
。
切片的零值是 nil
。nil slice 的長度和容量都是 0.
非 nil 的切片可以通過內建函數 make
創建:
a := make([]int, 0, 5) // len=0, cap=5
a := make([]int, 5) // len=cap=5
上面的 make 函數創建了一個以零值填充的數組並返回了指定的切片。這是一種預分配的方式,主要用於創建動態大小的數組。
多維切片:
ss := [][]int{
[]int{0, 1},
[]int{2, 3}
}
append:
var s []int
s = append(s, 1, 2)
Range:
pow = []int{1, 2, 4, 8, 16}
for i, v := range pow { // v 是 row[i] 的 copy
...
}
for i := range pow { // 第二個元素可以省略
...
}
for _, v := range pow { // 不需要的值可以賦值給 _
...
}
Map
map
的零值是 nil
,nil map 沒有鍵,也不能添加鍵。
可以使用 make
函數初始化一個可用的 map:
m := make(map(string)int) // map[KeyType]ValueType
m["a"] = 1 // insert or update
m1 := map[string]int { // literal
"b": 2,
}
fmt.Println(m["a"], m1["b"]) // retreive
delete(m, "a") // delete
elem, ok := m["x"] // test if a key exsists, type of ok is bool
function
函數也是可以作爲值來傳遞的,比如閉包。
func adder() func(int) int {
sum := 0
return func(x int) int {
sum += x
return sum
}
}
注意這裏使用了函數字面量,我嘗試使用命名函數但是失敗了。
string
這裏補充一些做習題時遇到的 string 的知識點:
字符串拼接簡單使用 +
即可,join
的話使用 strings.Join
方法
strings.Join([]string{"Hello", "world"}, ", ") // "Hello, world"
int 向 string 的轉換不能用 string()
,因爲這是 ASCII 編碼轉換,十進制字面轉換的話要用 strconv.Itoa
strconv.Itoa(123) // "123"
字符串支持切片,但不支持負數索引,即如果要去掉最後一個字符要寫成:
s[:s.Len()-1]
方法
go 沒有類
,但是允許給任何 type
定義方法。
type Point struct{
X, Y int
}
func (f Point) Distance() float64 {
return math.Sqrt(float64(f.X * f.X + f.Y * f.Y))
}
func main() {
p := Point{1, 1}
fmt.Println(p.Distance()) // 1.414...
}
方法與函數的區別是 func 後面添加了一個 “receiver” 參數,本質上仍然是一個函數。receiver 類似於 this
或者 self
。
receiver 的類型可以是指針,以此來實現修改原始值的目的。這時的 receiver 才更像是 self。
func (f *Point) Distance() float64 {
...
}
需注意,雖然這裏定義了指針 receiver,但你仍然可以用 p.Distance()
這樣的調用方式,就像之前那樣,go 會自動幫你轉成 (&p).Distance()
調用,理解爲語法糖即可。
接口
接口類型定義了一組方法,任何實現了這些方法的值都可以存放在接口變量裏。
type Reprer interface {
Repr nil
}
func (v *Point) Repr() nil {
fmt.Println(v)
}
var r Reprer
r = Point{1, 2}
對於 接口的值,一般可以理解爲存儲的是一個二元組 (value, type)
,如上例中的 (p, Point)
。值是複製的。
是否允許賦值的判斷,是去看 type 是否實現了接口的方法。
這裏存在一個情況是 type 合法但 value 爲 nil
時如何操作。此時若調用接口方法,傳入 recevier 的就是 nil(nil receiver
)。因此可以在方法裏做判斷:
if v == nil {
...
}
fmt.Println
接受的就是一個名爲 Stringer
的接口,類似 Python 的 __str__
方法:
type Stringer interface {
String() string
}
空接口 指的是沒有定義任何方法的接口,即:
type Anything interface {}
顯然這樣的接口可以賦予任意值。它可以用來存放類型不定的變量。
類型斷言
因爲接口存儲的類型不定,有時候需要對類型做判斷,可以使用一種類型斷言語法:
x := i.(T)
x, ok := i.(T)
第一種會引發 panic,第二種 ok 的值是 false.
針對 type 的 switch 語句是這樣的:
switch v := i.(type) {
case int:
...
default:
...
}
Errors
error type 是一個內建接口:
type error interface {
Error() string
}
函數可以定義爲返回 (value, error)
的元組:
func f() (int, error) {
...
}
這樣調用方就應該捕獲這個 error 並判斷其是否爲 nil
。
定義和使用一個具體的 error type 如下:
type ErrNegative float64
func (e, ErrNegative) Error() string {
return fmt.Sprintf("Negative num is invalid: %v", float64(e))
}
func raise() (float64, error) {
return 0, ErrNegative(-1)
}
需注意的是,在 Error 方法的定義中使用了 fmt.Sprintf
函數來格式化字符串,這時不能直接把 e
當參數丟進去,因爲這個函數需要調用 Error
本身,把 e 丟進去會導致無限遞歸,最終 Stack Overflow。
Reader
io.Reader
接口定義的是一個實現了 Read
方法的數據流。
func (T) Read(b []byte) (n int, err error)
以需要讀入的 byte 數組爲參數,返回剩餘待讀字節數和錯誤。一個常用的需求是對一個 Reader 進行包裝,不做任何處理的裸包裝如下:
type ProxyReader struct {
r io.Reader
}
func (r, ProxyReader) Read(b []byte) (int, error) {
n, err := r.r.Read(b)
return n, err
}
可以在裏面加特殊的處理代碼來實現特殊的 Reader 功能。
併發
Goroutines
創建協程的語法爲:go f()
。
Channels
所有協程共享同一個地址空間,因此 go 提供了一個頻道的概念來做協程間的數據傳遞和同步,操作符爲 <-
。
ch <- v // Send v to channel ch.
v := <-ch // Receive from ch, and
// assign value to v.
頻道是有類型的:
ch := make(chan int)
頻道的 send 和 receive 操作默認情況下都是阻塞的,這有利於實現自動的同步管理。
頻道也可以設置爲具有固定緩衝大小,給 make 傳入第二個參數即可:
ch := make(chan int, 100)
緩衝頻道滿了以後 send 會阻塞,空的時候 receive 會阻塞。
通過內建的 close 函數,頻道可以被關閉 close(c)
,這用於顯式的通知 receiver 沒有更多數據了。close 並不是必須的,可以按需使用。通常可以用於自動結束 range
頻道也支持 range 操作:
for v := range ch {
...
}
range 操作會在 ch 關閉後停止。
向已關閉的頻道發送數據會引發一個 panic。
select
channel 作爲一種 io 組件,也同樣支持 select 操作,與系統 select 類似,它會創建一個新的協程來監聽 channel 操作事件,當有 case 成功時即執行之,如果同時有多個滿足,則隨機選擇一個執行:
for {
select {
case x := <- ch1:
...
case y := <- ch2:
...
default:
...
}
}
default 語句會在沒有任何 case ready 的情況下執行,這通常用於做一些如超時處理的時鐘類操作。
sync.Mutex
Mutex 是 go 內建的一個協程鎖。典型用法如:
type SaValue struct {
value int
mux sync.Mutex
}
func (v *SaValue) Inc(num int) {
v.mux.Lock()
defer v.mux.Unlock()
v.value++
}