Go Tour

基礎

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++
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章