鍍金池/ 教程/ GO/ 進(jìn)階
版權(quán)
簡介
Bibliography
接口
通訊
函數(shù)
進(jìn)階
索引
并發(fā)

進(jìn)階

Go 有指針。然而卻沒有指針運(yùn)算,因此它們更象是引用而不是你所知道的來自于 C的指針。指針非常有用。在 Go 中調(diào)用函數(shù)的時(shí)候,得記得變量是值傳遞的。因此,為了修改一個(gè)傳遞入函數(shù)的值的效率和可能性,有了指針。

通過類型作為前綴來定義一個(gè)指針 ’’:var p int。現(xiàn)在 p 是一個(gè)指向整數(shù)值的指針。所有新定義的變量都被賦值為其類型的零值,而指針也一樣。一個(gè)新定義的或者沒有任何指向的指針,有值 nil。在其他語言中,這經(jīng)常被叫做空(NULL)指針,在 Go 中就是 nil。讓指針指向某些內(nèi)容,可以使用取址操(&),像這樣:

http://wiki.jikexueyuan.com/project/learn-go-language/images/111.png" alt="pic" />

從指針獲取值是通過在指針變量前置 ’*’ 實(shí)現(xiàn)的:

http://wiki.jikexueyuan.com/project/learn-go-language/images/112.png" alt="pic" />

前面已經(jīng)說了,沒有指針運(yùn)算,所以如果這樣寫:p++,它表示 (p)++:首先獲取指針指向的值,然后對這個(gè)值加一。

內(nèi)存分配

Go 同樣也垃圾收集,也就是說無須擔(dān)心內(nèi)存分配和回收。

Go 有兩個(gè)內(nèi)存分配原語,new 和 make。它們應(yīng)用于不同的類型,做不同的工作,可能有些迷惑人,但是規(guī)則很簡單。下面的章節(jié)展示了在 Go 中如何處理內(nèi)存分配,并且希望能夠讓 new 和 make 之間的區(qū)別更加清晰。

用 new 分配內(nèi)存

內(nèi)建函數(shù) new 本質(zhì)上說跟其他語言中的同名函數(shù)功能一樣:new(T) 分配了零值填充的 T 類型的內(nèi)存空間,并且返回其地址,一個(gè) *T 類型的值。用 Go 的術(shù)語說,它返回了一個(gè)指針,指向新分配的類型 T 的零值。記住這點(diǎn)非常重要。

這意味著使用者可以用 new 創(chuàng)建一個(gè)數(shù)據(jù)結(jié)構(gòu)的實(shí)例并且可以直接工作。如 bytes.Buffer 的文檔所述 “Buffer 的零值是一個(gè)準(zhǔn)備好了的空緩沖。” 類似的,sync.Mutex 也沒有明確的構(gòu)造函數(shù)或 Init 方法。取而代之, sync.Mutex 的零值被定義為非鎖定的互斥量。

零值是非常有用的。例如這樣的類型定義,57 頁的”定義自己的類型” 內(nèi)容。

http://wiki.jikexueyuan.com/project/learn-go-language/images/113.png" alt="pic" />

SyncedBuffer 的值在分配內(nèi)存或定義之后立刻就可以使用。在這個(gè)片段中,p 和 v 都可以在沒有任何更進(jìn)一步處理的情況下工作。

http://wiki.jikexueyuan.com/project/learn-go-language/images/114.png" alt="pic" />

用 make 分配內(nèi)存

回到內(nèi)存分配。內(nèi)建函數(shù) make(T, args) 與 new(T) 有著不同的功能。它只能創(chuàng)建 slice,map 和 channel,并且返回一個(gè)有初始值(非零)的 T 類型,而不是 *T。本質(zhì)來講,導(dǎo)致這三個(gè)類型有所不同的原因是指向數(shù)據(jù)結(jié)構(gòu)的引用在使用前必須被初始化。

例如,一個(gè) slice,是一個(gè)包含指向數(shù)據(jù)(內(nèi)部 array)的指針,長度和容量的三項(xiàng)描述符;在這些項(xiàng)目被初始化之前,slice 為 nil。對于 slice,map 和 channel,make 初始化了內(nèi)部的數(shù)據(jù)結(jié)構(gòu),填充適當(dāng)?shù)闹怠?/p>

例如,make([]int, 10, 100) 分配了 100 個(gè)整數(shù)的數(shù)組,然后用長度 10 和容量 100創(chuàng)建了 slice 結(jié)構(gòu)指向數(shù)組的前 10 個(gè)元素。區(qū)別是,new([]int) 返回指向新分配的內(nèi)存的指針,而零值填充的 slice 結(jié)構(gòu)是指向 nil 的 slice 值。

這個(gè)例子展示了 new 和 make 的不同。

http://wiki.jikexueyuan.com/project/learn-go-language/images/115.png" alt="pic" />

務(wù)必記得 make 僅適用于 map,slice 和 channel,并且返回的不是指針。應(yīng)當(dāng)用 new 獲得特定的指針。

new 分配;make 初始化

上面的兩段可以簡單總結(jié)為:

  • new(T) 返回 *T 指向一個(gè)零值 T
  • make(T) 返回初始化后的 T

當(dāng)然 make 僅適用于slice,map 和channel。

構(gòu)造函數(shù)與復(fù)合聲明

有時(shí)零值不能滿足需求,必須要有一個(gè)用于初始化的構(gòu)造函數(shù),例如這個(gè)來自 os 包的例子。

http://wiki.jikexueyuan.com/project/learn-go-language/images/116.png" alt="pic" />

有許多冗長的內(nèi)容。可以使用復(fù)合聲明使其更加簡潔,每次只用一個(gè)表達(dá)式創(chuàng)建一個(gè)新的實(shí)例。

http://wiki.jikexueyuan.com/project/learn-go-language/images/117.png" alt="pic" />

返回本地變量的地址沒有問題;在函數(shù)返回后,相關(guān)的存儲區(qū)域仍然存在。

事實(shí)上,從復(fù)合聲明獲取分配的實(shí)例的地址更好,因此可以最終將兩行縮短到一行。

return &File{fd, name, nil, 0}

The items (called of a composite +literal are laid out in order and must all be 所有的項(xiàng)目(稱作字段)都必須按順序全部寫上。然而,通過對元素用字段: 值成對的標(biāo)識,初始化內(nèi)容可以按任意順序出現(xiàn),并且可以省略初始化為零值的字段。因此可以這樣

return &File{fd: fd, name: name}

在特定的情況下,如果復(fù)合聲明不包含任何字段,它創(chuàng)建特定類型的零值。表達(dá)式 new(File) 和 &File{} 是等價(jià)的。

復(fù)合聲明同樣可以用于創(chuàng)建 array,slice 和 map,通過指定適當(dāng)?shù)乃饕?map 鍵來標(biāo)識字段。在這個(gè)例子中,無論是 Enone,Eio 還是 Einval 初始化都能很好的工作,只要確保它們不同就好了。

http://wiki.jikexueyuan.com/project/learn-go-language/images/118.png" alt="pic" />

定義自己的類型

自然,Go 允許定義新的類型,通過關(guān)鍵字type 實(shí)現(xiàn):

type foo i n t

創(chuàng)建了一個(gè)新的類型 foo 作用跟 int 一樣。創(chuàng)建更加復(fù)雜的類型需要用到 struct 關(guān)鍵字。這有個(gè)在一個(gè)數(shù)據(jù)結(jié)構(gòu)中記錄某人的姓名(string)和年齡(int),并且使其成為一個(gè)新的類型的例子:

http://wiki.jikexueyuan.com/project/learn-go-language/images/119.png" alt="pic" />

通常,fmt.Printf("%v\n", a) 的輸出是

&{Pete 42}

這很棒!Go 知道如何打印結(jié)構(gòu)。如果僅想打印某一個(gè),或者某幾個(gè)結(jié)構(gòu)中的字段,需要使用 .。例如,僅僅打印名字:

http://wiki.jikexueyuan.com/project/learn-go-language/images/120.png" alt="pic" />

結(jié)構(gòu)字段

之前已經(jīng)提到結(jié)構(gòu)中的項(xiàng)目被稱為 field。沒有字段的結(jié)構(gòu):struct {} 或者有四個(gè) c 字段的:

http://wiki.jikexueyuan.com/project/learn-go-language/images/121.png" alt="pic" />

如果省略字段的名字,可以創(chuàng)建匿名字段,例如:

http://wiki.jikexueyuan.com/project/learn-go-language/images/122.png" alt="pic" />

注意首字母大寫的字段可以被導(dǎo)出,也就是說,在其他包中可以進(jìn)行讀寫。字段名以小寫字母開頭是當(dāng)前包的私有的。包的函數(shù)定義是類似的,參閱第 3 章了解更多細(xì)節(jié)。

方法

可以對新定義的類型創(chuàng)建函數(shù)以便操作,可以通過兩種途徑:

  1. 創(chuàng)建一個(gè)函數(shù)接受這個(gè)類型的參數(shù)。
func doSomething(n1 *NameAge, n2 i n t ) { /* */ }

(你可能已經(jīng)猜到了)這是函數(shù)調(diào)用。

2 .創(chuàng)建一個(gè)工作在這個(gè)類型上的函數(shù)(參閱在2.1 中定義的接收方):

func (n1 *NameAge) doSomething(n2 i n t ) { /* */ }

這是方法調(diào)用,可以類似這樣使用:

var n *NameAge
n.doSomething(2)

使用函數(shù)還是方法是由程序員決定的,但是如果想要滿足接口(參閱下一章)就只能使用方法。如果沒有這方面的需求,那就由個(gè)人品味決定了。

使用函數(shù)還是方法完全是由程序員說了算,但是若需要滿足接口(參看下一章)就必須使用方法。如果沒有這樣的需求,那就完全由習(xí)慣來決定是使用函數(shù)還是方法了。

但是下面的內(nèi)容一定要留意,引用自 [10]:

如果 x 可獲取地址,并且 &x 的方法中包含了 m,x.m() 是 (&x).m() 更短的寫法。

根據(jù)上面所述,這意味著下面的情況不是錯(cuò)誤:

http://wiki.jikexueyuan.com/project/learn-go-language/images/123.png" alt="pic" />

這里 Go 會查找 NameAge 類型的變量n 的方法列表,沒有找到就會再查找 *NameAge 類型的方法列表,并且將其轉(zhuǎn)化為 (&n).doSomething(2)。

下面的類型定義中有一些微小但是很重要的不同之處。同時(shí)可以參閱 [10, section “Type Declarations”]。假設(shè)有:

// Mutex 數(shù)據(jù)類型有兩個(gè)方法,Lock 和 Unlock。

http://wiki.jikexueyuan.com/project/learn-go-language/images/124.png" alt="pic" />

現(xiàn)在用兩種不同的風(fēng)格創(chuàng)建了兩個(gè)數(shù)據(jù)類型。

  • type NewMutex Mutex;
  • type PrintableMutex struct {Mutex }.

現(xiàn)在 NewMutux 等同于 Mutex,但是它沒有任何 Mutex 的方法。換句話說,它的方法是空的。

但是 PrintableMutex 已經(jīng)從 Mutex 繼承了方法集合。如同 [10] 所說:

*PrintableMutex 的方法集合包含了 Lock 和 Unlock 方法,被綁定到其匿名字段 Mutex。

轉(zhuǎn)換

有時(shí)需要將一個(gè)類型轉(zhuǎn)換為另一個(gè)類型。在 Go 中可以做到,不過有一些規(guī)則。首先,將一個(gè)值轉(zhuǎn)換為另一個(gè)是由操作符(看起來像函數(shù):byte())完成的,并且不是所有的轉(zhuǎn)換都是允許的。

Table 4.1. 合法的轉(zhuǎn)換,float64 同 float32 類似。注意,為了適配表格的顯示,float32被簡寫為 flt32。

http://wiki.jikexueyuan.com/project/learn-go-language/images/125.png" alt="pic" />

  • 從 string 到字節(jié)或者 ruin 的 slice。
mystring := "hello this is string"  
byteslice := []byte(mystring)

轉(zhuǎn)換到 byte slice,每個(gè) byte 保存字符串對應(yīng)字節(jié)的整數(shù)值。注意 Go 的字符串是 UTF-8 編碼的,一些字符可能是 1、2、3 或者 4 個(gè)字節(jié)結(jié)尾。

runeslice := []rune(mystring)

轉(zhuǎn)換到 rune slice,每個(gè) rune 保存 Unicode 編碼的指針。字符串中的每個(gè)字符對應(yīng)一個(gè)整數(shù)。

  • 從字節(jié)或者整形的 slice 到 string。
b := []byte {'h','e','l','l','o'} // 復(fù)合聲明
s := s t r i n g (b)
i := []rune {257,1024,65}
r := s t r i n g (i)

對于數(shù)值,定義了下面的轉(zhuǎn)換:

  • 將整數(shù)轉(zhuǎn)換到指定的(bit)長度:uint8(int);
  • 從浮點(diǎn)數(shù)到整數(shù):int(float32)。這會截?cái)喔↑c(diǎn)數(shù)的小數(shù)部分;
  • 其他的類似:float32(int)。

用戶定義類型的轉(zhuǎn)換

如何在自定義類型之間進(jìn)行轉(zhuǎn)換?這里創(chuàng)建了兩個(gè)類型 Foo 和 Bar,而 Bar 是 Foo 的一個(gè)別名:

http://wiki.jikexueyuan.com/project/learn-go-language/images/126.png" alt="pic" />

然后:

http://wiki.jikexueyuan.com/project/learn-go-language/images/127.png" alt="pic" />

最后一行會引起錯(cuò)誤:

cannot use b (type bar) as type foo in assignment(不能使用 b(類型 bar)作為類型 foo 賦值)

這可以通過轉(zhuǎn)換來修復(fù):

var f foo = foo(b)

注意轉(zhuǎn)換那些字段不一致的結(jié)構(gòu)是相當(dāng)困難的。同時(shí)注意,轉(zhuǎn)換 b 到 int 同樣會出錯(cuò);整數(shù)與有整數(shù)字段的結(jié)構(gòu)并不一樣。

組合

TODO(miek):work in progress Go 不是面向?qū)ο笳Z言,因此并沒有繼承。但是有時(shí)又會需要從已經(jīng)實(shí)現(xiàn)的類型中“繼承”并修改一些方法。在Go 中可以用嵌入一個(gè)類型的方式來實(shí)現(xiàn)。

練習(xí)

Q17. (1) 指針運(yùn)算

  1. 在正文的第 54 頁有這樣的文字:

…這里沒有指針運(yùn)算,因此如果這樣寫:p++,它被解釋為(p)++:
首先解析引用然后增加值。

當(dāng)像這樣增加一個(gè)值的時(shí)候,什么類型可以工作?

2 .為什么它不能工作在所有類型上?

Q18. (2) 使用 interface 的 map 函數(shù)

  1. 使用練習(xí) Q11 的答案,利用 interface 使其更加通用。讓它至少能同時(shí)工作于 int 和 string。

Q19. (1) 指針

  1. 假設(shè)定義了下面的結(jié)構(gòu):

http://wiki.jikexueyuan.com/project/learn-go-language/images/128.png" alt="pic" />

下面兩行之間的區(qū)別是什么?

var p1 Person
p2 := new(Person)

2 .下面兩個(gè)內(nèi)存分配的區(qū)別是什么?

http://wiki.jikexueyuan.com/project/learn-go-language/images/129.png" alt="pic" />

http://wiki.jikexueyuan.com/project/learn-go-language/images/130.png" alt="pic" />

Q20. (1) Linked List

1 .Make use of the package container/list to create a (doubly) linked list. Push the values 1, 2 and 4 to the list and then print it.

2 .Create your own linked list implementation. And perform the same actions as in question 1

Q21. (1) Cat

  1. 編寫一個(gè)程序,模仿 Unix 的 cat 程序。對于不知道這個(gè)程序的人來說,下面的調(diào)用顯示了文件 blah 的內(nèi)容:% cat blah
  2. 使其支持 n 開關(guān),用于輸出每行的行號。
  3. 上面問題中,1 提供的解決方案存在一個(gè) Bug。你能定位并修復(fù)它嗎?

Q22. (2) 方法調(diào)用

  1. 假設(shè)有下面的程序。要注意的是包 container/vector 曾經(jīng)是 Go 的一部分,但是當(dāng)內(nèi)建的 append 出現(xiàn)后,就被移除了。然而,對于當(dāng)前的問題這不重要。這個(gè)包實(shí)現(xiàn)了有 push 和 pop 方法的棧結(jié)構(gòu)。

http://wiki.jikexueyuan.com/project/learn-go-language/images/131.png" alt="pic" />

k1,k2 和 k3 的類型是什么?

  1. 當(dāng)前,這個(gè)程序可以編譯并且運(yùn)行良好。在不同類型的變量上 Push 都可以工作。Push 的文檔這樣描述:

func (p *IntVector) Push(x int) Push 增加 x 到向量的末尾。

那么接受者應(yīng)當(dāng)是 *IntVector 類型,為什么上面的代碼(Push 語句)可以正確工作?above (the Push statements) work correct then?

答案

A17. (1) 指針運(yùn)算

  1. 這僅能工作于指向數(shù)字(int, uint 等等)的指針值。
  2. ++ 僅僅定義在數(shù)字類型上,同時(shí)由于在 Go 中沒有運(yùn)算符重載,所以會在其他類型上失?。ň幾g錯(cuò)誤)。

A18. (2) 使用 interface 的 map 函數(shù)

http://wiki.jikexueyuan.com/project/learn-go-language/images/132.png" alt="pic" />

A19. (1) 指針

  1. 第一行:var p1 Person 分配了 Person-值給 p1。p1 的類型是 Person。
    第二行:p2 := new(Person) 分配了內(nèi)存并且將指針賦值給 p2。p2 的類型是 *Person。
  2. 在第二個(gè)函數(shù)中,x 指向一個(gè)新的(堆上分配的)變量 t,其包含了實(shí)際參數(shù)值的副本。

在第一個(gè)函數(shù)中,x 指向了 t 指向的內(nèi)容,也就是實(shí)際上的參數(shù)指向的內(nèi)容。

因此在第二個(gè)函數(shù),我們有了“額外” 的變量存儲了相關(guān)值的副本。

A20. (1) Linked List

  1. The following is the implementation of a program using doubly linked lists from container/list.

http://wiki.jikexueyuan.com/project/learn-go-language/images/133.png" alt="pic" />

http://wiki.jikexueyuan.com/project/learn-go-language/images/134.png" alt="pic" />

http://wiki.jikexueyuan.com/project/learn-go-language/images/135.png" alt="pic" />

http://wiki.jikexueyuan.com/project/learn-go-language/images/136.png" alt="pic" />

http://wiki.jikexueyuan.com/project/learn-go-language/images/137.png" alt="pic" />

0 .Include all the packages we need.

  1. Declare a type for the value our list will contain;
  2. declare a type for the each node in our list;
  3. Mimic the interface of container/list.
  4. When pushing, create a new Node with the provided value;
  5. if the list is empty, put the new node at the head;
  6. otherwise put it at the tail;
  7. make sure the new node points back to the previously existing one;
  8. point tail to the newly inserted node.
  9. When popping, return an error if the list is empty;
  10. otherwise save the last value;
  11. discard the last node from the list;
  12. and make sure the list is consistent if it becomes empty;

A21. (1) Cat

  1. 下面是 cat 的實(shí)現(xiàn),同樣支持 n 輸出每行的行號。

http://wiki.jikexueyuan.com/project/learn-go-language/images/138.png" alt="pic" />

http://wiki.jikexueyuan.com/project/learn-go-language/images/139.png" alt="pic" />

http://wiki.jikexueyuan.com/project/learn-go-language/images/140.png" alt="pic" />

  1. 包含所有需要用到的包;
  2. 定義新的開關(guān) "n”,默認(rèn)是關(guān)閉的。注意很容易寫的幫助文本;
  3. 實(shí)際上讀取并且顯示文件內(nèi)容的函數(shù);
  4. 每次讀一行;
  5. 如果到達(dá)文件結(jié)尾;
  6. 如果設(shè)定了行號,打印行號然后是內(nèi)容本身;
  7. 否則,僅僅打印該行內(nèi)容。

  8. 當(dāng)最后一行不包括換行符時(shí),這個(gè) Bug 就會出現(xiàn)。更糟糕的情況是,當(dāng)輸入只有一行且沒有換行符的時(shí)候,什么也不顯示。下面的程序是一個(gè)更好的解決方案。

http://wiki.jikexueyuan.com/project/learn-go-language/images/141.png" alt="pic" />

http://wiki.jikexueyuan.com/project/learn-go-language/images/142.png" alt="pic" />

A22. (2) 方法調(diào)用

  1. k1 的類型是 vector.IntVector。為什么?這里使用了符號 {},因此獲得了類型的值。變量 k2 是 vector.IntVector,因?yàn)楂@得了復(fù)合語句的地址(&)。而最后的 k3 同樣是 vector.IntVector 類型,因?yàn)?new 返回該類型的指針。

  2. 在 [10] 的“調(diào)用” 章節(jié),有這樣的描述:

當(dāng) x 的方法集合包含 m,并且參數(shù)列表可以賦值給 m 的參數(shù),方法調(diào)用 x.m() 是合法的。如果 x 可以被地址化,而 &x 的方法集合包含 m,x.m() 可以作為 (&x).m() 的省略寫法。

換句話說,由于 k1 可以被地址化,而 *vector.IntVector 具有 Push 方法,調(diào)用 k1.Push(2) 被 Go 轉(zhuǎn)換為 (&k1).Push(2) 來使型系統(tǒng)愉悅(也使你愉悅——現(xiàn)在你已經(jīng)了解到這一點(diǎn))

上一篇:接口下一篇:Bibliography