mirror of
https://github.com/gopl-zh/gopl-zh.github.com.git
synced 2024-12-26 06:46:27 +00:00
ch13 review
This commit is contained in:
parent
983524dbc8
commit
427ac25df9
@ -14,6 +14,6 @@ Go語言的另一支祖先,帶來了Go語言區别其他語言的重要特性
|
|||||||
|
|
||||||
接下來,Rob Pike和其他人開始不斷嚐試將CSP引入實際的編程語言中。他們第一次嚐試引入CSP特性的編程語言叫Squeak(老鼠間交流的語言),是一個提供鼠標和鍵盤事件處理的編程語言,它的管道是靜態創建的。然後是改進版的Newsqueak語言,提供了類似C語言語句和表達式的語法和類似Pascal語言的推導語法。Newsqueak是一個帶垃圾迴收的純函數式語言,它再次針對鍵盤、鼠標和窗口事件管理。但是在Newsqueak語言中管道是動態創建的,屬於第一類值, 可以保存到變量中。
|
接下來,Rob Pike和其他人開始不斷嚐試將CSP引入實際的編程語言中。他們第一次嚐試引入CSP特性的編程語言叫Squeak(老鼠間交流的語言),是一個提供鼠標和鍵盤事件處理的編程語言,它的管道是靜態創建的。然後是改進版的Newsqueak語言,提供了類似C語言語句和表達式的語法和類似Pascal語言的推導語法。Newsqueak是一個帶垃圾迴收的純函數式語言,它再次針對鍵盤、鼠標和窗口事件管理。但是在Newsqueak語言中管道是動態創建的,屬於第一類值, 可以保存到變量中。
|
||||||
|
|
||||||
在Plan9操作繫統中,這些優秀的想法被吸收到了一個叫Alef的編程語言中。Alef試圖將Newsqueak語言改造爲繫統編程語言,但是因爲缺少垃圾迴收機製而導致併發編程很痛苦。(譯註:在Aelf之後還有一個叫Limbo的編程語言,Go語言從其中借鑒了很多特性。在docs目録包含了這些語言相關的文檔手冊。)
|
在Plan9操作繫統中,這些優秀的想法被吸收到了一個叫Alef的編程語言中。Alef試圖將Newsqueak語言改造爲繫統編程語言,但是因爲缺少垃圾迴收機製而導致併發編程很痛苦。(譯註:在Aelf之後還有一個叫Limbo的編程語言,Go語言從其中借鑒了很多特性。在docs目録包含了這些語言相關的文檔手冊。 具體請參考Pike的講稿:http://talks.golang.org/2012/concurrency.slide#9 )
|
||||||
|
|
||||||
Go語言的其他的一些特性零散地來自於其他一些編程語言;比如iota語法是從APL語言借鑒,詞法作用域與嵌套函數來自於Scheme語言(和其他很多語言)。當然,我們也可以從Go中發現很多創新的設計。比如Go語言的切片爲動態數組提供了有效的隨機存取的性能,這可能會讓人聯想到鏈表的底層的共享機製。還有Go語言新發明的defer語句。
|
Go語言的其他的一些特性零散地來自於其他一些編程語言;比如iota語法是從APL語言借鑒,詞法作用域與嵌套函數來自於Scheme語言(和其他很多語言)。當然,我們也可以從Go中發現很多創新的設計。比如Go語言的切片爲動態數組提供了有效的隨機存取的性能,這可能會讓人聯想到鏈表的底層的共享機製。還有Go語言新發明的defer語句。
|
||||||
|
@ -1,45 +1,42 @@
|
|||||||
## 13.1. unsafe.Sizeof, Alignof 和 Offsetof
|
## 13.1. unsafe.Sizeof, Alignof 和 Offsetof
|
||||||
|
|
||||||
`unsafe.Sizeof` 函數返迴操作數在內存的字節大小, 可以是任意類型的表達式, 但是併不會對表達式進行求值. `Sizeof` 是一個 uintptr 類型的常量表達式, 因此返迴的結果可以用着數據的大小, 或者用作計算其他的常量.
|
unsafe.Sizeof函數返迴操作數在內存中的字節大小,參數可以是任意類型的表達式,但是它併不會對表達式進行求值。一個Sizeof函數調用是一個對應uintptr類型的常量表達式,因此返迴的結果可以用作數組類型的長度大小,或者用作計算其他的常量。
|
||||||
|
|
||||||
```Go
|
```Go
|
||||||
import "unsafe"
|
import "unsafe"
|
||||||
fmt.Println(unsafe.Sizeof(float64(0))) // "8"
|
fmt.Println(unsafe.Sizeof(float64(0))) // "8"
|
||||||
```
|
```
|
||||||
|
|
||||||
`Sizeof` 隻返迴數據結構中固定的部分, 例如字符串中指針和字符串長度部分, 但是併不包含字符串的內容. Go中非聚合類型通常有一個固定的尺寸, 盡管不同工具鏈的具體大小可能會有所不同. 考慮到可移植性, 引用類型或包含引用類型的大小在32位平颱上是4個字節, 在64位平颱上是8個字節.
|
Sizeof函數返迴的大小隻包括數據結構中固定的部分,例如字符串對應結構體中的指針和字符串長度部分,但是併不包含指針指向的字符串的內容。Go語言中非聚合類型通常有一個固定的大小,盡管在不同工具鏈下生成的實際大小可能會有所不同。考慮到可移植性,引用類型或包含引用類型的大小在32位平颱上是4個字節,在64位平颱上是8個字節。
|
||||||
|
|
||||||
計算機加載和保存數據時, 如果內存地址合理地對齊的將會更有效率.
|
計算機在加載和保存數據時,如果內存地址合理地對齊的將會更有效率。例如2字節大小的int16類型的變量地址應該是偶數,一個4字節大小的rune類型變量的地址應該是4的倍數,一個8字節大小的float64、uint64或64-bit指針類型變量的地址應該是8字節對齊的。但是對於再大的地址對齊倍數則是不需要的,卽使是complex128等較大的數據類型最多也隻是8字節對齊。
|
||||||
例如 2 字節大小的 int16 類型應該是偶數, 一個4 字節大小的 rune 類型地址應該是 4 的倍數, 一個 8 字節大小的 float64, uint64 或 64-bit 指針 的地址應該是 8 字節對齊的. 但是對於再大的地址對齊倍數則是不需要的,
|
|
||||||
卽使是 complex128 等較大的數據類型.
|
|
||||||
|
|
||||||
由於這個因素,一個聚合類型(結構體或數組)的大小至少是所有字段或元素大小的總和, 或者更大因爲可能存在空洞. 空洞是編譯器自動添加的沒有被使用的空間, 用於保證後面每個字段或元素的地址相對於結構或數組的開始地址能夠合理地對齊.
|
由於地址對齊這個因素,一個聚合類型(結構體或數組)的大小至少是所有字段或元素大小的總和,或者更大因爲可能存在內存空洞。內存空洞是編譯器自動添加的沒有被使用的內存空間,用於保證後面每個字段或元素的地址相對於結構或數組的開始地址能夠合理地對齊(譯註:內存空洞可能會存在一些隨機數據,可能會對用unsafe包直接操作內存的處理産生影響)。
|
||||||
|
|
||||||
|
|
||||||
類型 | 大小
|
類型 | 大小
|
||||||
----------------------------- | ----
|
----------------------------- | ----
|
||||||
bool | 1字節
|
bool | 1個字節
|
||||||
intN, uintN, floatN, complexN | N/8字節 (例如 float64 是 8字節)
|
intN, uintN, floatN, complexN | N/8個字節(例如float64是8個字節)
|
||||||
int, uint, uintptr | 1個機器字
|
int, uint, uintptr | 1個機器字
|
||||||
*T | 1個機器字
|
*T | 1個機器字
|
||||||
string | 2個機器字(data,len)
|
string | 2個機器字(data,len)
|
||||||
[]T | 3個機器字(data,len, cap)
|
[]T | 3個機器字(data,len,cap)
|
||||||
map | 1個機器字
|
map | 1個機器字
|
||||||
func | 1個機器字
|
func | 1個機器字
|
||||||
chan | 1個機器字
|
chan | 1個機器字
|
||||||
interface | 2個機器字(type,value)
|
interface | 2個機器字(type,value)
|
||||||
|
|
||||||
Go的語言規范併沒有保證一個字段的聲明順序和內存中的順序是一致的, 所以理論上一個編譯器可以隨意地重新排列每個字段的內存布局, 隨着在寫作本書的時候編譯器還沒有這麽做. 下面的三個結構體有着相同的字段, 但是第一個比另外的兩個需要多 50% 的內存.
|
Go語言的規范併沒有要求一個字段的聲明順序和內存中的順序是一致的,所以理論上一個編譯器可以隨意地重新排列每個字段的內存位置,隨然在寫作本書的時候編譯器還沒有這麽做。下面的三個結構體雖然有着相同的字段,但是第一種寫法比另外的兩個需要多50%的內存。
|
||||||
|
|
||||||
|
|
||||||
```Go
|
```Go
|
||||||
// 64-bit 32-bit
|
// 64-bit 32-bit
|
||||||
struct{ bool; float64; int16 } // 3 words 4words
|
struct{ bool; float64; int16 } // 3 words 4words
|
||||||
struct{ float64; int16; bool } // 2 words 3words
|
struct{ float64; int16; bool } // 2 words 3words
|
||||||
struct{ bool; int16; float64 } // 2 words 3words
|
struct{ bool; int16; float64 } // 2 words 3words
|
||||||
```
|
```
|
||||||
|
|
||||||
雖然關於對齊算法的細節超齣了本書的范圍, 也不是每一個結構體都需要擔心這個問題, 不過有效的包裝可以使數據結構更加緊湊, 內存使用率和性能都可能受益.
|
關於內存地址對齊算法的細節超齣了本書的范圍,也不是每一個結構體都需要擔心這個問題,不過有效的包裝可以使數據結構更加緊湊(譯註:未來的Go語言編譯器應該會默認優化結構體的順序,當然用於應該也能夠指定具體的內存布局,相同討論請參考 [Issue10014](https://github.com/golang/go/issues/10014) ),內存使用率和性能都可能會受益。
|
||||||
|
|
||||||
`unsafe.Alignof` 函數返迴對應參數的類型需要對齊的倍數. 和 Sizeof 類似, Alignof 也是返迴一個常量表達式, 對應一個常量. 通常情況下布爾和數字類型需要對齊到它們本身的大小(最多8個字節), 其它的類型對齊到機器字大小.
|
`unsafe.Alignof` 函數返迴對應參數的類型需要對齊的倍數. 和 Sizeof 類似, Alignof 也是返迴一個常量表達式, 對應一個常量. 通常情況下布爾和數字類型需要對齊到它們本身的大小(最多8個字節), 其它的類型對齊到機器字大小.
|
||||||
|
|
||||||
@ -55,14 +52,11 @@ var x struct {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
The table below shows the results of applying the three unsafe functions to x itself and to each of its three fields:
|
下面顯示了對x和它的三個字段調用unsafe包相關函數的計算結果:
|
||||||
|
|
||||||
下面顯示了應用三個函數對 x 和它的三個字段計算的結果:
|
|
||||||
|
|
||||||
![](../images/ch13-01.png)
|
![](../images/ch13-01.png)
|
||||||
|
|
||||||
|
32位繫統:
|
||||||
32位繫統:
|
|
||||||
|
|
||||||
```
|
```
|
||||||
Sizeof(x) = 16 Alignof(x) = 4
|
Sizeof(x) = 16 Alignof(x) = 4
|
||||||
@ -71,7 +65,7 @@ Sizeof(x.b) = 2 Alignof(x.b) = 2 Offsetof(x.b) = 2
|
|||||||
Sizeof(x.c) = 12 Alignof(x.c) = 4 Offsetof(x.c) = 4
|
Sizeof(x.c) = 12 Alignof(x.c) = 4 Offsetof(x.c) = 4
|
||||||
```
|
```
|
||||||
|
|
||||||
64位繫統:
|
64位繫統:
|
||||||
|
|
||||||
```
|
```
|
||||||
Sizeof(x) = 32 Alignof(x) = 8
|
Sizeof(x) = 32 Alignof(x) = 8
|
||||||
@ -80,6 +74,5 @@ Sizeof(x.b) = 2 Alignof(x.b) = 2 Offsetof(x.b) = 2
|
|||||||
Sizeof(x.c) = 24 Alignof(x.c) = 8 Offsetof(x.c) = 8
|
Sizeof(x.c) = 24 Alignof(x.c) = 8 Offsetof(x.c) = 8
|
||||||
```
|
```
|
||||||
|
|
||||||
雖然它們在不安全的 unsafe 包, 但是這幾個函數併不是眞的不安全,
|
雖然這幾個函數在不安全的unsafe包,但是這幾個函數調用併不是眞的不安全,特别在需要優化內存空間時它們返迴的結果對於理解原生的內存布局很有幫助。
|
||||||
特别在需要優化內存空間時它們對於理解原生的內存布局很有幫助.
|
|
||||||
|
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
## 13.2. unsafe.Pointer
|
## 13.2. unsafe.Pointer
|
||||||
|
|
||||||
大多數指針類型寫成 *T, 含義是 "一個指向T類型變量的指針". `unsafe.Pointer` 是特别定義的一種指針類型, 它可以包含任意類型變量的地址. 當然, 我們不可以直接使用 *p 穫取 `unsafe.Pointer` 指針指向的眞實變量, 因爲我們併不知道變量的類型. 和普通指針一樣, `unsafe.Pointer` 指針是可以比較的, 支持和 nil 比較判斷是否爲空指針.
|
大多數指針類型會寫成`*T`,表示是“一個指向T類型變量的指針”。unsafe.Pointer是特别定義的一種指針類型(譯註:類似C語言中的`void*`類型的指針),它可以包含任意類型變量的地址。當然,我們不可以直接通過`*p`來穫取unsafe.Pointer指針指向的眞實變量的值,因爲我們併不知道變量的具體類型。和普通指針一樣,unsafe.Pointer指針也是可以比較的,併且支持和nil常量比較判斷是否爲空指針。
|
||||||
|
|
||||||
一個普通的 *T 類型指針可以被轉化爲 `unsafe.Pointer` 類型指針, 併且一個 `unsafe.Pointer` 類型指針也可以被轉迴普通指針, 也可以是和 *T 不同類型的指針. 通過將 `*float64` 類型指針 轉化爲 `*uint64` 類型指針, 我們可以檢査一個浮點數變量的位模式.
|
一個普通的`*T`類型指針可以被轉化爲unsafe.Pointer類型指針,併且一個unsafe.Pointer類型指針也可以被轉迴普通的指針,被轉迴普通的指針類型併不需要和原始的`*T`類型相同。通過將`*float64`類型指針轉化爲`*uint64`類型指針,我們可以査看一個浮點數變量的位模式。
|
||||||
|
|
||||||
```Go
|
```Go
|
||||||
package math
|
package math
|
||||||
@ -12,13 +12,11 @@ func Float64bits(f float64) uint64 { return *(*uint64)(unsafe.Pointer(&f)) }
|
|||||||
fmt.Printf("%#016x\n", Float64bits(1.0)) // "0x3ff0000000000000"
|
fmt.Printf("%#016x\n", Float64bits(1.0)) // "0x3ff0000000000000"
|
||||||
```
|
```
|
||||||
|
|
||||||
通過新指針, 我們可以更新浮點數的位模式. 通過位模式操作浮點數是可以的, 但是更重要的意義是指針轉換讓我們可以在不破壞類型繫統的前提下向內存寫入任意的值.
|
通過轉爲新類型指針,我們可以更新浮點數的位模式。通過位模式操作浮點數是可以的,但是更重要的意義是指針轉換語法讓我們可以在不破壞類型繫統的前提下向內存寫入任意的值。
|
||||||
|
|
||||||
一個 `unsafe.Pointer` 指針也可以被轉化爲 uintptr 類似, 然後保存到指針型數值變量中, 用以做必要的指針運算.
|
一個unsafe.Pointer指針也可以被轉化爲uintptr類型,然後保存到指針型數值變量中(譯註:這隻是和當前指針相同的一個數字值,併不是一個指針),然後用以做必要的指針數值運算。(第三章內容,uintptr是一個無符號的整型數,足以保存一個地址)這種轉換雖然也是可逆的,但是將uintptr轉爲unsafe.Pointer指針可能會破壞類型繫統,因爲併不是所有的數字都是有效的內存地址。
|
||||||
(第三章內容, uintptr是一個無符號的整型數, 足有保存一個地址.)
|
|
||||||
這種轉換也是可逆的, 但是, 將 uintptr 轉爲 `unsafe.Pointer` 指針可能破壞類型繫統, 因爲併不是所有的數字都是有效的內存地址.
|
|
||||||
|
|
||||||
許多將 `unsafe.Pointer` 指針 轉爲原生數字, 然後再轉爲 `unsafe.Pointer` 指針的操作是不安全的. 下面的例子需要將變量 x 的地址加上 b 字段的偏移轉化爲 *int16 類型指針, 然後通過該指針更新 `x.b`:
|
許多將unsafe.Pointer指針轉爲原生數字,然後再轉迴爲unsafe.Pointer類型指針的操作也是不安全的。比如下面的例子需要將變量x的地址加上b字段地址偏移量轉化爲`*int16`類型指針,然後通過該指針更新x.b:
|
||||||
|
|
||||||
```Go
|
```Go
|
||||||
//gopl.io/ch13/unsafeptr
|
//gopl.io/ch13/unsafeptr
|
||||||
@ -36,24 +34,30 @@ pb := (*int16)(unsafe.Pointer(
|
|||||||
fmt.Println(x.b) // "42"
|
fmt.Println(x.b) // "42"
|
||||||
```
|
```
|
||||||
|
|
||||||
盡管寫法很繁瑣, 但在這里併不是一件壞事, 因爲這些功能應該很謹慎地使用. 不要試圖將引入可能而破壞代碼的正確性的 uintptr 臨時變量. 下面段代碼是不正確的:
|
上面的寫法盡管很繁瑣,但在這里併不是一件壞事,因爲這些功能應該很謹慎地使用。不要試圖引入一個uintptr類型的臨時變量,因爲它可能會破壞代碼的安全性(譯註:這是眞正可以體會unsafe包爲何不安全的例子)。下面段代碼是錯誤的:
|
||||||
|
|
||||||
錯誤的原因很微妙. 有時候垃圾迴收器會移動一些變量以降低內存碎片的問題.這類垃圾迴收器被稱爲移動GC. 當一個變量被移動, 所有的保存改變量舊地址的指針必鬚同時被更新爲變量移動後的新地址. 從垃圾收集器的視角來看, 一個 `unsafe.Pointer` 是一個指針, 因此當變量被移動是對應的指針必鬚被更新, 但是 `uintptr` 隻是一個普通的數字, 所以其值不應該被改變. 上面錯誤的代碼因爲一個非指針的臨時變量 `tmp`, 導致垃圾收集器無法正確識别這個是一個指向變量 `x` 的指針. 第二個語句執行時, 變量 `x` 可能已經被轉移, 臨時變量 `tmp` 也就不在對應現在的 `&x.b`. 第三個賦值語句將徹底摧譭那個之前的那部分內存空間.
|
```Go
|
||||||
|
// NOTE: subtly incorrect!
|
||||||
|
tmp := uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b)
|
||||||
|
pb := (*int16)(unsafe.Pointer(tmp))
|
||||||
|
*pb = 42
|
||||||
|
```
|
||||||
|
|
||||||
有很多類似原因導致的錯誤. 例如這條語句:
|
産生錯誤的原因很微妙。有時候垃圾迴收器會移動一些變量以降低內存碎片等問題。這類垃圾迴收器被稱爲移動GC。當一個變量被移動,所有的保存改變量舊地址的指針必鬚同時被更新爲變量移動後的新地址。從垃圾收集器的視角來看,一個unsafe.Pointer是一個指向變量的指針,因此當變量被移動是對應的指針也必鬚被更新;但是uintptr類型的臨時變量隻是一個普通的數字,所以其值不應該被改變。上面錯誤的代碼因爲引入一個非指針的臨時變量tmp,導致垃圾收集器無法正確識别這個是一個指向變量x的指針。當第二個語句執行時,變量x可能已經被轉移,這時候臨時變量tmp也就不再是現在的`&x.b`地址。第三個向之前無效地址空間的賦值語句將徹底摧譭整個程序!
|
||||||
|
|
||||||
|
還有很多類似原因導致的錯誤。例如這條語句:
|
||||||
|
|
||||||
```Go
|
```Go
|
||||||
pT := uintptr(unsafe.Pointer(new(T))) // 提示: 錯誤!
|
pT := uintptr(unsafe.Pointer(new(T))) // 提示: 錯誤!
|
||||||
```
|
```
|
||||||
|
|
||||||
這里併沒有指針引用 `new` 新創建的變量, 因此語句執行完成之後, 垃圾收集器有權迴收其內存空間, 所以返迴的 `pT` 保存將是無效的地址.
|
這里併沒有指針引用`new`新創建的變量,因此該語句執行完成之後,垃圾收集器有權馬上迴收其內存空間,所以返迴的pT將是無效的地址。
|
||||||
|
|
||||||
目前的Go語言實現還沒有使用移動GC(未來可能實現), 但這不該是僥幸的理由: 當前的Go實現已經有移動變量的場景. 在5.2節我們提到goroutine的棧是根據需要動態增長的. 當這個時候, 原來棧中的所以變量可能需要被移動到新的更大的棧中, 所以我們無法確保變量的地址在整個使用週期內保持不變.
|
雖然目前的Go語言實現還沒有使用移動GC(譯註:未來可能實現),但這不該是編寫錯誤代碼僥幸的理由:當前的Go語言實現已經有移動變量的場景。在5.2節我們提到goroutine的棧是根據需要動態增長的。當發送棧動態增長的時候,原來棧中的所以變量可能需要被移動到新的更大的棧中,所以我們併不能確保變量的地址在整個使用週期內是不變的。
|
||||||
|
|
||||||
在編寫本文時, 還沒有清晰的原則就指引Go程序員, 什麽樣 `unsafe.Pointer` 和 `uintptr` 的轉換是不安全的(參考 [Go issue7192](https://github.com/golang/go/issues/7192). 譯註: 該問題已經脩複.), 因此我們強烈建議按照最壞的方式處理. 將所有包含變量 `y` 地址的 `uintptr` 類型變量當作 BUG 處理, 同時減少不必要的 `unsafe.Pointer` 到 `uintptr` 的轉換. 在第一個例子中, 有三個到 `uintptr` 的轉換, 字段偏移量的運算, 所有的轉換全在一個表達式完成.
|
在編寫本文時,還沒有清晰的原則來指引Go程序員,什麽樣的unsafe.Pointer和uintptr的轉換是不安全的(參考 [Go issue7192](https://github.com/golang/go/issues/7192 ). 譯註: 該問題已經關閉),因此我們強烈建議按照最壞的方式處理。將所有包含變量地址的uintptr類型變量當作BUG處理,同時減少不必要的unsafe.Pointer類型到uintptr類型的轉換。在第一個例子中,有三個轉換——字段偏移量到uintptr的轉換和轉迴unsafe.Pointer類型的操作——所有的轉換全在一個表達式完成。
|
||||||
|
|
||||||
當調用一個庫函數, 併且返迴的是 `uintptr` 類型是, 比如下面反射包中的相關函數,
|
當調用一個庫函數,併且返迴的是uintptr類型地址時(譯註:普通方法實現的函數不盡量不要返迴該類型。下面例子是reflect包的函數,reflect包和unsafe包一樣都是采用特殊技術實現的,編譯器可能給它們開了後門),比如下面反射包中的相關函數,返迴的結果應該立卽轉換爲unsafe.Pointer以確保指針指向的是相同的變量。
|
||||||
返迴的結果應該立卽轉換爲 `unsafe.Pointer` 以確保指針指向的是相同的變量.
|
|
||||||
|
|
||||||
```Go
|
```Go
|
||||||
package reflect
|
package reflect
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
## 13.3. 示例: 深度相等判斷
|
## 13.3. 示例: 深度相等判斷
|
||||||
|
|
||||||
來自 reflect 包的 DeepEqual 對兩個值進行深度相等判斷. DeepEqual 使用內建的 `==` 操作符對基礎類型進行相等判斷, 對於複合類型則遞歸變量每個基礎類型然後做類似的比較判斷. 因爲它工作在任意的類型上, 甚至對一些不支持 `==` 操作符的類型也可以工作, 因此在一些測試代碼中被廣泛地使用. 比如下面的代碼是用 DeepEqual 比較兩個字符串數組是否等價.
|
來自reflect包的DeepEqual函數可以對兩個值進行深度相等判斷。DeepEqual函數使用內建的==比較操作符對基礎類型進行相等判斷,對於複合類型則遞歸該變量的每個基礎類型然後做類似的比較判斷。因爲它可以工作在任意的類型上,甚至對於一些不支持==操作運算符的類型也可以工作,因此在一些測試代碼中廣泛地使用該函數。比如下面的代碼是用DeepEqual函數比較兩個字符串數組是否相等。
|
||||||
|
|
||||||
```Go
|
```Go
|
||||||
func TestSplit(t *testing.T) {
|
func TestSplit(t *testing.T) {
|
||||||
@ -10,9 +10,7 @@ func TestSplit(t *testing.T) {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
盡管 DeepEqual 很方便, 而且可以支持任意的類型, 但是也有不足之處.
|
盡管DeepEqual函數很方便,而且可以支持任意的數據類型,但是它也有不足之處。例如,它將一個nil值的map和非nil值但是空的map視作不相等,同樣nil值的slice 和非nil但是空的slice也視作不相等。
|
||||||
例如, 它將一個 nil map 和 非 nil 的空的 map 視作不相等,
|
|
||||||
同樣 nil slice 和 非 nil 的空的 slice 也不相等.
|
|
||||||
|
|
||||||
```Go
|
```Go
|
||||||
var a, b []string = nil, []string{}
|
var a, b []string = nil, []string{}
|
||||||
@ -22,7 +20,7 @@ var c, d map[string]int = nil, make(map[string]int)
|
|||||||
fmt.Println(reflect.DeepEqual(c, d)) // "false"
|
fmt.Println(reflect.DeepEqual(c, d)) // "false"
|
||||||
```
|
```
|
||||||
|
|
||||||
在這里定義一個自己的 Equal 函數用於比較人員的值. 和 DeepEqual 類似的是它也是基於 slice 和 map 的元素進行遞歸比較, 不同之處是它將 nil slice(map類似) 和非 nil 的空 slice 視作相等的值. 基礎部分的比較可以基於反射完成, 和 12.3 章的 Display 實現方法類似. 同樣, 我們頂一個一個內部函數 equal, 用於內部的遞歸比較. 目前不用關心 seen 參數. 對於每一對需要比較的 x 和 y, equal 函數 首先檢測它們是否都有效(或都無效), 然後檢測它們是否是相同的類型. 剩下的部分是一個大的 switch 分支, 用於擁有相同基礎類型的比較. 因爲頁面空間的限製, 我們省略了一些類似的分支.
|
我們希望在這里實現一個自己的Equal函數,用於比較類型的值。和DeepEqual函數類似的地方是它也是基於slice和map的每個元素進行遞歸比較,不同之處是它將nil值的slice(map類似)和非nil值但是空的slice視作相等的值。基礎部分的比較可以基於reflect包完成,和12.3章的Display函數的實現方法類似。同樣,我們也定義了一個內部函數equal,用於內部的遞歸比較。讀者目前不用關心seen參數的具體含義。對於每一對需要比較的x和y,equal函數首先檢測它們是否都有效(或都無效),然後檢測它們是否是相同的類型。剩下的部分是一個鉅大的switch分支,用於相同基礎類型的元素比較。因爲頁面空間的限製,我們省略了一些相似的分支。
|
||||||
|
|
||||||
```Go
|
```Go
|
||||||
gopl.io/ch13/equal
|
gopl.io/ch13/equal
|
||||||
@ -65,8 +63,7 @@ func equal(x, y reflect.Value, seen map[comparison]bool) bool {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
和前面的建議一樣, 我們不公開使用反射相關的接口,
|
和前面的建議一樣,我們併不公開reflect包相關的接口,所以導齣的函數需要在內部自己將變量轉爲reflect.Value類型。
|
||||||
所以導齣的函數需要在內部自己將變量轉爲 reflect.Value 類型.
|
|
||||||
|
|
||||||
```Go
|
```Go
|
||||||
// Equal reports whether x and y are deeply equal.
|
// Equal reports whether x and y are deeply equal.
|
||||||
@ -81,7 +78,7 @@ type comparison struct {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
爲了確保算法對於循環數據結構也能正常退齣, 我們必鬚記録每次已經比較的變量, 從而避免進入第二次的比較. Equal 函數分配了一組用於比較的結構體, 包含每對比較對象的地址(unsafe.Pointer形式保存)和類型. 我們記録類型的原因是, 有些不同的變量可能對應相同的地址. 例如, 如果 x 和 y 都是數組類型, 那麽 x 和 `x[0]` 將對應相同的地址, y 和 `y[0]` 也是對應相同的地址, 這可以用於判斷 對x 和 y 比較 或 x[0] 和 y[0] 的是否進行過了.
|
爲了確保算法對於有環的數據結構也能正常退齣,我們必鬚記録每次已經比較的變量,從而避免進入第二次的比較。Equal函數分配了一組用於比較的結構體,包含每對比較對象的地址(unsafe.Pointer形式保存)和類型。我們要記録類型的原因是,有些不同的變量可能對應相同的地址。例如,如果x和y都是數組類型,那麽x和x[0]將對應相同的地址,y和y[0]也是對應相同的地址,這可以用於區分x與y之間的比較或x[0]與y[0]之間的比較是否進行過了。
|
||||||
|
|
||||||
```Go
|
```Go
|
||||||
// cycle check
|
// cycle check
|
||||||
@ -99,7 +96,7 @@ if x.CanAddr() && y.CanAddr() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
這是 Equal 函數的使用的例子:
|
這是Equal函數用法的例子:
|
||||||
|
|
||||||
```Go
|
```Go
|
||||||
fmt.Println(Equal([]int{1, 2, 3}, []int{1, 2, 3})) // "true"
|
fmt.Println(Equal([]int{1, 2, 3}, []int{1, 2, 3})) // "true"
|
||||||
@ -108,7 +105,7 @@ fmt.Println(Equal([]string(nil), []string{})) // "true"
|
|||||||
fmt.Println(Equal(map[string]int(nil), map[string]int{})) // "true"
|
fmt.Println(Equal(map[string]int(nil), map[string]int{})) // "true"
|
||||||
```
|
```
|
||||||
|
|
||||||
它甚至可以處理類似12.3章中導致Display陷入死循環的數據.
|
Equal函數甚至可以處理類似12.3章中導致Display陷入陷入死循環的帶有環的數據。
|
||||||
|
|
||||||
```Go
|
```Go
|
||||||
// Circular linked lists a -> b -> a and c -> c.
|
// Circular linked lists a -> b -> a and c -> c.
|
||||||
@ -125,8 +122,8 @@ fmt.Println(Equal(a, b)) // "false"
|
|||||||
fmt.Println(Equal(a, c)) // "false"
|
fmt.Println(Equal(a, c)) // "false"
|
||||||
```
|
```
|
||||||
|
|
||||||
```
|
**練習 13.1:** 定義一個深比較函數,對於十億以內的數字比較,忽略類型差異。
|
||||||
練習 13.1: 定義一個深比較函數, 對於十億以內的數字比較, 忽略類型差異.
|
|
||||||
練習 13.2: 編寫一個函數, 報告其參數是否循環數據結構.
|
**練習 13.2:** 編寫一個函數,報告其參數是否循環數據結構。
|
||||||
```
|
|
||||||
|
|
||||||
|
104
ch13/ch13-04.md
104
ch13/ch13-04.md
@ -1,10 +1,10 @@
|
|||||||
## 13.4. 通過cgo調用C代碼
|
## 13.4. 通過cgo調用C代碼
|
||||||
|
|
||||||
Go程序可能會遇到要訪問C語言的某些硬件驅動的場景, 或者是從一個C++實現的嵌入式數據庫査詢記録的場景, 或者是使用Fortran實現的一些線性代數庫的場景. C作爲一個通用語言, 很多庫會選擇提供一個C兼容的API, 然後用其他語言實現.
|
Go程序可能會遇到要訪問C語言的某些硬件驅動函數的場景,或者是從一個C++語言實現的嵌入式數據庫査詢記録的場景,或者是使用Fortran語言實現的一些線性代數庫的場景。C語言作爲一個通用語言,很多庫會選擇提供一個C兼容的API,然後用其他不同的編程語言實現(譯者:Go語言需要也應該擁抱這些鉅大的代碼遺産)。
|
||||||
|
|
||||||
在本節中, 我們將構建一個簡易的數據壓縮程序, 通過使用一個Go語言自帶的叫cgo的用於支援C語言函數調用的工具. 這類工具被稱爲外圍函數接口(ffi), 併且cgo也不是Go中唯一的類似工具. SWIG(swig.org) 是類似的另一個被廣泛使用的工具, 它提供了很多複雜特性以支援C++的集成, 但 SWIG 不是這里要討論的主題.
|
在本節中,我們將構建一個簡易的數據壓縮程序,使用了一個Go語言自帶的叫cgo的用於支援C語言函數調用的工具。這類工具一般被稱爲 *foreign-function interfaces* (簡稱ffi), 併且在類似工具中cgo也不是唯一的。SWIG( http://swig.org )是另一個類似的且被廣泛使用的工具,SWIG提供了很多複雜特性以支援C++的特性,但SWIG併不是我們要討論的主題。
|
||||||
|
|
||||||
在標準庫的 `compress/...` 子目録有很多流行的壓縮算法的編碼和解碼實現, 包括LZW壓縮算法(Unix的compress命令用的算法)和DEFLATE壓縮算法(GNU gzip命令用的算法). 這些包的API的細節有些差異, 但是它們都提供了針對 `io.Writer` 的壓縮接口, 和提供了針對 `io.Reader` 的解壓縮接口. 例如:
|
在標準庫的`compress/...`子包有很多流行的壓縮算法的編碼和解碼實現,包括流行的LZW壓縮算法(Unix的compress命令用的算法)和DEFLATE壓縮算法(GNU gzip命令用的算法)。這些包的API的細節雖然有些差異,但是它們都提供了針對 io.Writer類型輸齣的壓縮接口和提供了針對io.Reader類型輸入的解壓縮接口。例如:
|
||||||
|
|
||||||
```Go
|
```Go
|
||||||
package gzip // compress/gzip
|
package gzip // compress/gzip
|
||||||
@ -12,15 +12,34 @@ func NewWriter(w io.Writer) io.WriteCloser
|
|||||||
func NewReader(r io.Reader) (io.ReadCloser, error)
|
func NewReader(r io.Reader) (io.ReadCloser, error)
|
||||||
```
|
```
|
||||||
|
|
||||||
bzip2壓縮算法, 是基於優雅的 Burrows-Wheeler 變換, 運行速度比 gzip 要慢, 但是可以提供更高的壓縮比. 標準庫的 `compress/bzip2` 包目前還沒有提供 bzip2 算法的壓縮實現. 完全從頭實現是一個繁瑣的工作, 而且 bzip.org 有現成的 libbzip2 開源實現, 文檔齊全而且性能較好,
|
bzip2壓縮算法,是基於優雅的Burrows-Wheeler變換算法,運行速度比gzip要慢,但是可以提供更高的壓縮比。標準庫的compress/bzip2包目前還沒有提供bzip2壓縮算法的實現。完全從頭開始實現是一個壓縮算法是一件繁瑣的工作,而且 http://bzip.org 已經有現成的libbzip2的開源實現,不僅文檔齊全而且性能又好。
|
||||||
|
|
||||||
如果C庫比較小, 我們可以用純Go重新實現一遍. 如果我們對性能沒有特殊要求, 我們可以用 `os/exec` 包的方法將C編寫的應用程序作爲一個子進行運行. 隻有當你需要使用複雜但是性能更高的底層C接口時, 就是使用cgo的場景了. 下面我們將通過一個例子講述cgo的用法.
|
如果是比較小的C語言庫,我們完全可以用純Go語言重新實現一遍。如果我們對性能也沒有特殊要求的話,我們還可以用os/exec包的方法將C編寫的應用程序作爲一個子進程運行。隻有當你需要使用複雜而且性能更高的底層C接口時,就是使用cgo的場景了(譯註:用os/exec包調用子進程的方法會導致程序運行時依賴那個應用程序)。下面我們將通過一個例子講述cgo的具體用法。
|
||||||
|
|
||||||
要使用 libbzip2, 我們需要一個 `bz_stream` 結構體, 用於保持輸入和輸齣緩存.
|
譯註:本章采用的代碼都是最新的。因爲之前已經齣版的書中包含的代碼隻能在Go1.5之前使用。從Go1.6開始,Go語言已經明確規定了哪些Go語言指針可以之間傳入C語言函數。新代碼重點是增加了bz2alloc和bz2free的兩個函數,用於bz_stream對象空間的申請和釋放操作。下面是新代碼中增加的註釋,説明這個問題:
|
||||||
然後有三個函數: BZ2_bzCompressInit 用於初始化緩存, BZ2_bzCompress 用於將輸入緩存的數據壓縮到輸齣緩存, BZ2_bzCompressEnd 用於釋放不需要的緩存.
|
|
||||||
(目前不要擔心包的具體結構, 這個例子的目的就是演示各個部分如何組合在一起的)
|
|
||||||
|
|
||||||
我們可以在Go代碼中直接調用 BZ2_bzCompressInit 和 BZ2_bzCompressEnd, 但是對於 BZ2_bzCompress, 我們將定義一個C語言的包裝函數, 爲了顯示他是如何完成的. 下面是C代碼, 對應一個獨立的文件.
|
```Go
|
||||||
|
// The version of this program that appeared in the first and second
|
||||||
|
// printings did not comply with the proposed rules for passing
|
||||||
|
// pointers between Go and C, described here:
|
||||||
|
// https://github.com/golang/proposal/blob/master/design/12416-cgo-pointers.md
|
||||||
|
//
|
||||||
|
// The rules forbid a C function like bz2compress from storing 'in'
|
||||||
|
// and 'out' (pointers to variables allocated by Go) into the Go
|
||||||
|
// variable 's', even temporarily.
|
||||||
|
//
|
||||||
|
// The version below, which appears in the third printing, has been
|
||||||
|
// corrected. To comply with the rules, the bz_stream variable must
|
||||||
|
// be allocated by C code. We have introduced two C functions,
|
||||||
|
// bz2alloc and bz2free, to allocate and free instances of the
|
||||||
|
// bz_stream type. Also, we have changed bz2compress so that before
|
||||||
|
// it returns, it clears the fields of the bz_stream that contain
|
||||||
|
// pointers to Go variables.
|
||||||
|
```
|
||||||
|
|
||||||
|
要使用libbzip2,我們需要先構建一個bz_stream結構體,用於保持輸入和輸齣緩存。然後有三個函數:BZ2_bzCompressInit用於初始化緩存,BZ2_bzCompress用於將輸入緩存的數據壓縮到輸齣緩存,BZ2_bzCompressEnd用於釋放不需要的緩存。(目前不要擔心包的具體結構, 這個例子的目的就是演示各個部分如何組合在一起的。)
|
||||||
|
|
||||||
|
我們可以在Go代碼中直接調用BZ2_bzCompressInit和BZ2_bzCompressEnd,但是對於BZ2_bzCompress,我們將定義一個C語言的包裝函數,用它完成眞正的工作。下面是C代碼,對應一個獨立的文件。
|
||||||
|
|
||||||
```C
|
```C
|
||||||
gopl.io/ch13/bzip
|
gopl.io/ch13/bzip
|
||||||
@ -38,12 +57,12 @@ int bz2compress(bz_stream *s, int action,
|
|||||||
int r = BZ2_bzCompress(s, action);
|
int r = BZ2_bzCompress(s, action);
|
||||||
*inlen -= s->avail_in;
|
*inlen -= s->avail_in;
|
||||||
*outlen -= s->avail_out;
|
*outlen -= s->avail_out;
|
||||||
|
s->next_in = s->next_out = NULL;
|
||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
現在讓我們轉到Go部分, 第一部分如下所示. 其中 `import "C"` 的語句是比較特别的. 其實併沒有一個叫 `C` 的包, 但是這行語句會讓Go構建在編譯之前先運行cgo工具.
|
現在讓我們轉到Go語言部分,第一部分如下所示。其中`import "C"`的語句是比較特别的。其實併沒有一個叫C的包,但是這行語句會讓Go編譯程序在編譯之前先運行cgo工具。
|
||||||
|
|
||||||
|
|
||||||
```Go
|
```Go
|
||||||
// Package bzip provides a writer that uses bzip2 compression (bzip.org).
|
// Package bzip provides a writer that uses bzip2 compression (bzip.org).
|
||||||
@ -53,8 +72,11 @@ package bzip
|
|||||||
#cgo CFLAGS: -I/usr/include
|
#cgo CFLAGS: -I/usr/include
|
||||||
#cgo LDFLAGS: -L/usr/lib -lbz2
|
#cgo LDFLAGS: -L/usr/lib -lbz2
|
||||||
#include <bzlib.h>
|
#include <bzlib.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
bz_stream* bz2alloc() { return calloc(1, sizeof(bz_stream)); }
|
||||||
int bz2compress(bz_stream *s, int action,
|
int bz2compress(bz_stream *s, int action,
|
||||||
char *in, unsigned *inlen, char *out, unsigned *outlen);
|
char *in, unsigned *inlen, char *out, unsigned *outlen);
|
||||||
|
void bz2free(bz_stream* s) { free(s); }
|
||||||
*/
|
*/
|
||||||
import "C"
|
import "C"
|
||||||
|
|
||||||
@ -71,20 +93,48 @@ type writer struct {
|
|||||||
|
|
||||||
// NewWriter returns a writer for bzip2-compressed streams.
|
// NewWriter returns a writer for bzip2-compressed streams.
|
||||||
func NewWriter(out io.Writer) io.WriteCloser {
|
func NewWriter(out io.Writer) io.WriteCloser {
|
||||||
const (
|
const blockSize = 9
|
||||||
blockSize = 9
|
const verbosity = 0
|
||||||
verbosity = 0
|
const workFactor = 30
|
||||||
workFactor = 30
|
w := &writer{w: out, stream: C.bz2alloc()}
|
||||||
)
|
|
||||||
w := &writer{w: out, stream: new(C.bz_stream)}
|
|
||||||
C.BZ2_bzCompressInit(w.stream, blockSize, verbosity, workFactor)
|
C.BZ2_bzCompressInit(w.stream, blockSize, verbosity, workFactor)
|
||||||
return w
|
return w
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
在循環的每次迭代中, 向bz2compress傳入數據的地址和剩餘部分的長度, 還有輸齣緩存 w.outbuf 的地址和容量. 這兩個長度信息通過它們的地址傳入而不是值傳入, 因爲bz2compress函數可能會根據已經壓縮的數據和壓縮後數據的大小來更新這兩個值(譯註: 這里的用法有問題, 勘誤已經提到. 具體脩複的方法稍後再補充). 每個塊壓縮後的數據被寫入到底層的 io.Writer.
|
在預處理過程中,cgo工具爲生成一個臨時包用於包含所有在Go語言中訪問的C語言的函數或類型。例如C.bz_stream和C.BZ2_bzCompressInit。cgo工具通過以某種特殊的方式調用本地的C編譯器來發現在Go源文件導入聲明前的註釋中包含的C頭文件中的內容(譯註:`import "C"語句前僅捱着的註釋是對應cgo的特殊語法,對應必要的構建參數選項和C語言代碼`)。
|
||||||
|
|
||||||
Close 方法和 Write 方法有着類似的結構, 通過一個循環將剩餘的壓縮數據刷新到輸齣緩存.
|
在cgo註釋中還可以包含#cgo指令,用於給C語言工具鏈指定特殊的參數。例如CFLAGS和LDFLAGS分别對應傳給C語言編譯器的編譯參數和鏈接器參數,使它們可以特定目録找到bzlib.h頭文件和libbz2.a庫文件。這個例子假設你已經在/usr目録成功安裝了bzip2庫。如果bzip2庫是安裝在不同的位置,你需要更新這些參數。
|
||||||
|
|
||||||
|
NewWriter函數通過調用C語言的BZ2_bzCompressInit函數來初始化stream中的緩存。在writer結構中還包括了另一個buffer,用於輸齣緩存。
|
||||||
|
|
||||||
|
下面是Write方法的實現,返迴成功壓縮數據的大小,主體是一個循環中調用C語言的bz2compress函數實現的。從代碼可以看到,Go程序可以訪問C語言的bz_stream、char和uint類型,還可以訪問bz2compress等函數,甚至可以訪問C語言中像BZ_RUN那樣的宏定義,全部都是以C.x語法訪問。其中C.uint類型和Go語言的uint類型併不相同,卽使它們具有相同的大小也是不同的類型。
|
||||||
|
|
||||||
|
```Go
|
||||||
|
func (w *writer) Write(data []byte) (int, error) {
|
||||||
|
if w.stream == nil {
|
||||||
|
panic("closed")
|
||||||
|
}
|
||||||
|
var total int // uncompressed bytes written
|
||||||
|
|
||||||
|
for len(data) > 0 {
|
||||||
|
inlen, outlen := C.uint(len(data)), C.uint(cap(w.outbuf))
|
||||||
|
C.bz2compress(w.stream, C.BZ_RUN,
|
||||||
|
(*C.char)(unsafe.Pointer(&data[0])), &inlen,
|
||||||
|
(*C.char)(unsafe.Pointer(&w.outbuf)), &outlen)
|
||||||
|
total += int(inlen)
|
||||||
|
data = data[inlen:]
|
||||||
|
if _, err := w.w.Write(w.outbuf[:outlen]); err != nil {
|
||||||
|
return total, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return total, nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
在循環的每次迭代中,向bz2compress傳入數據的地址和剩餘部分的長度,還有輸齣緩存w.outbuf的地址和容量。這兩個長度信息通過它們的地址傳入而不是值傳入,因爲bz2compress函數可能會根據已經壓縮的數據和壓縮後數據的大小來更新這兩個值。每個塊壓縮後的數據被寫入到底層的io.Writer。
|
||||||
|
|
||||||
|
Close方法和Write方法有着類似的結構,通過一個循環將剩餘的壓縮數據刷新到輸齣緩存。
|
||||||
|
|
||||||
```Go
|
```Go
|
||||||
// Close flushes the compressed data and closes the stream.
|
// Close flushes the compressed data and closes the stream.
|
||||||
@ -95,6 +145,7 @@ func (w *writer) Close() error {
|
|||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
C.BZ2_bzCompressEnd(w.stream)
|
C.BZ2_bzCompressEnd(w.stream)
|
||||||
|
C.bz2free(w.stream)
|
||||||
w.stream = nil
|
w.stream = nil
|
||||||
}()
|
}()
|
||||||
for {
|
for {
|
||||||
@ -111,11 +162,11 @@ func (w *writer) Close() error {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
壓縮完成後, Close 用了 defer 確保函數退齣前調用 C.BZ2_bzCompressEnd 釋放輸入和輸齣流的緩存. 此刻 `w.stream` 指針將不在有效, 我們將它設置爲 nil 以保證安全, 然後在每個方法中增加 nil 檢測, 以防止用戶在關閉後依然錯誤使用相關方法.
|
壓縮完成後,Close方法用了defer函數確保函數退齣前調用C.BZ2_bzCompressEnd和C.bz2free釋放相關的C資源。此刻w.stream指針將不在有效,我們將它設置爲nil以保證安全,然後在每個方法中增加了nil檢測,以防止用戶在關閉後依然錯誤使用相關方法。
|
||||||
|
|
||||||
不僅僅寫是非併發安全的, 甚至併發調用 Close 和 Write 也可能導致C代碼的崩潰. 脩複這個問題是 練習13.3 的內容.
|
上面的實現中,不僅僅寫是非併發安全的,甚至併發調用Close和Write方法也可能導致程序的的崩潰。脩複這個問題是練習13.3的內容。
|
||||||
|
|
||||||
下面的bzipper程序是使用我們自己包實現的bzip2壓縮命令. 它的行爲和許多Unix繫統的 bzip2 命令類似.
|
下面的bzipper程序,使用我們自己包實現的bzip2壓縮命令。它的行爲和許多Unix繫統的bzip2命令類似。
|
||||||
|
|
||||||
```Go
|
```Go
|
||||||
gopl.io/ch13/bzipper
|
gopl.io/ch13/bzipper
|
||||||
@ -141,7 +192,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
在上面的場景中, 我們使用 bzipper 壓縮了 /usr/share/dict/words 繫統自帶的詞典, 從 938,848 字節壓縮到 335,405 字節, 大於是原始大小的三分之一. 然後使用繫統自帶的bunzip2命令進行解壓. 壓縮前後文件的SHA256哈希碼是相同了, 這也説明了我們的壓縮工具是可用的. (如果你的繫統沒有sha256sum命令, 那麽請先按照 練習4.2 實現一個類似的工具)
|
在上面的場景中,我們使用bzipper壓縮了/usr/share/dict/words繫統自帶的詞典,從938,848字節壓縮到335,405字節。大約是原始數據大小的三分之一。然後使用繫統自帶的bunzip2命令進行解壓。壓縮前後文件的SHA256哈希碼是相同了,這也説明了我們的壓縮工具是正確的。(如果你的繫統沒有sha256sum命令,那麽請先按照練習4.2實現一個類似的工具)
|
||||||
|
|
||||||
```
|
```
|
||||||
$ go build gopl.io/ch13/bzipper
|
$ go build gopl.io/ch13/bzipper
|
||||||
@ -155,8 +206,9 @@ $ ./bzipper < /usr/share/dict/words | bunzip2 | sha256sum
|
|||||||
126a4ef38493313edc50b86f90dfdaf7c59ec6c948451eac228f2f3a8ab1a6ed -
|
126a4ef38493313edc50b86f90dfdaf7c59ec6c948451eac228f2f3a8ab1a6ed -
|
||||||
```
|
```
|
||||||
|
|
||||||
我們演示了將一個C庫鏈接到Go程序. 相反, 將Go編譯爲靜態庫然後鏈接到C程序, 或者將Go編譯爲動態庫然後在C程序中動態加載也都是可行的. 這里我們隻展示的cgo很小的一些方面, 更多的關於內存管理, 指針, 迴調函數, 信號處理, 字符串, errno處理, 終結器, 以及 goroutines 和繫統線程的關繫等, 有很多細節可以討論. 特别是如何將Go的指針傳入C函數的規則也是異常複雜的, 部分的原因在 13.2節 有討論到, 但是在Go1.5中還沒有被明確. 如果要進一步閲讀, 可以從 https://golang.org/cmd/cgo 開始.
|
我們演示了如何將一個C語言庫鏈接到Go語言程序。相反, 將Go編譯爲靜態庫然後鏈接到C程序,或者將Go程序編譯爲動態庫然後在C程序中動態加載也都是可行的(譯註:在Go1.5中,Windows繫統的Go語言實現併不支持生成C語言動態庫或靜態庫的特性。不過好消息是,目前已經有人在嚐試解決這個問題,具體請訪問 [Issue11058](https://github.com/golang/go/issues/11058) )。這里我們隻展示的cgo很小的一些方面,更多的關於內存管理、指針、迴調函數、中斷信號處理、字符串、errno處理、終結器,以及goroutines和繫統線程的關繫等,有很多細節可以討論。特别是如何將Go語言的指針傳入C函數的規則也是異常複雜的(譯註:簡單來説,要傳入C函數的Go指針指向的數據本身不能包含指針或其他引用類型;併且C函數在返迴後不能繼續持有Go指針;併且在C函數返迴之前,Go指針是被鎖定的,不能導致對應指針數據被移動或棧的調整),部分的原因在13.2節有討論到,但是在Go1.5中還沒有被明確(譯註:Go1.6將會明確cgo中的指針使用規則)。如果要進一步閲讀,可以從 https://golang.org/cmd/cgo 開始。
|
||||||
|
|
||||||
**練習13.3:** 使用 sync.Mutex 以保證 bzip2.writer 在多個 goroutines 中被併發調用是安全的.
|
**練習 13.3:** 使用sync.Mutex以保證bzip2.writer在多個goroutines中被併發調用是安全的。
|
||||||
|
|
||||||
|
**練習 13.4:** 因爲C庫依賴的限製。 使用os/exec包啟動/bin/bzip2命令作爲一個子進程,提供一個純Go的bzip.NewWriter的替代實現(譯註:雖然是純Go實現,但是運行時將依賴/bin/bzip2目録,其他操作繫統可能無法運行)。
|
||||||
|
|
||||||
**練習13.4:** 因爲C庫依賴的限製. 使用 `os/exec` 包啟動 `/bin/bzip2` 命令作爲一個子進程, 提供一個純Go的 bzip.NewWriter 的替代實現.
|
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
## 13.5. 幾點忠告
|
## 13.5. 幾點忠告
|
||||||
|
|
||||||
我們在前一章結尾的時候, 我們警告要謹慎使用反射. 那些警告同樣適用於本章的 unsafe 包.
|
我們在前一章結尾的時候,我們警告要謹慎使用reflect包。那些警告同樣適用於本章的unsafe包。
|
||||||
|
|
||||||
高級語言使得程序員不用在關繫眞正運行程序的指令細節, 同時也不再需要關註許多如內部布局之類的無關實現細節. 因爲這個絶緣的抽象層, 我們可以編寫安全健壯的, 併且可以運行在不同操作繫統上的具有高度可移植性的程序.
|
高級語言使得程序員不用在關心眞正運行程序的指令細節,同時也不再需要關註許多如內存布局之類的實現細節。因爲高級語言這個絶緣的抽象層,我們可以編寫安全健壯的,併且可以運行在不同操作繫統上的具有高度可移植性的程序。
|
||||||
|
|
||||||
但是 unsafe 包, 讓程序員可以透過這個絶緣的抽象層使用使用一些必要的功能, 或者是爲了更高的性能. 代價就是犧牲了可移植性和程序安全, 因此使用 unsafe 是一個危險的行爲. 我們對何時以及如何使用unsafe包的建議和我們在11.5節提到的Knuth對過早優化的建議類似. 大多數Go程序員可能永遠不會需要直接使用unsafe包. 當然, 永遠都會有一些用 unsafe 包實現會更簡單的場景. 如果確實認爲使用 unsafe 包是最理想的方式, 那麽應該盡可能將它限製較小的范圍, 那樣其他代碼忽略unsafe的影響.
|
但是unsafe包,它讓程序員可以透過這個絶緣的抽象層直接使用一些必要的功能,雖然可能是爲了穫得更好的性能。但是代價就是犧牲了可移植性和程序安全,因此使用unsafe包是一個危險的行爲。我們對何時以及如何使用unsafe包的建議和我們在11.5節提到的Knuth對過早優化的建議類似。大多數Go程序員可能永遠不會需要直接使用unsafe包。當然,也永遠都會有一些需要使用unsafe包實現會更簡單的場景。如果確實認爲使用unsafe包是最理想的方式,那麽應該盡可能將它限製在較小的范圍,那樣其它代碼就忽略unsafe的影響。
|
||||||
|
|
||||||
現在, 把最後兩章拋入腦後吧. 編寫一些實在的應用. 遠離reflect的unsafe包, 除非你確實需要它們.
|
現在,趕緊將最後兩章拋入腦後吧。編寫一些實實在在的應用是眞理。請遠離reflect的unsafe包,除非你確實需要它們。
|
||||||
|
|
||||||
|
最後,用Go快樂地編程。我們希望你能像我們一樣喜歡Go語言。
|
||||||
|
|
||||||
用Go快樂地編程. 我們希望你能像我們一樣喜歡Go語言.
|
|
||||||
|
19
ch13/ch13.md
19
ch13/ch13.md
@ -1,21 +1,20 @@
|
|||||||
# 第13章 底層編程
|
# 第13章 底層編程
|
||||||
|
|
||||||
Go的設計包含了諸多安全策略, 限製了可能導致程序錯誤的用法. 編譯時類型檢査檢測可以發現大多數類型不匹配的變量操作, 例如兩個字符串做減法的錯誤. 字符串, 字典, 切片 和管道等所有的內置類型, 都有嚴格的類型轉換規則.
|
Go語言的設計包含了諸多安全策略,限製了可能導致程序運行齣現錯誤的用法。編譯時類型檢査檢査可以發現大多數類型不匹配的操作,例如兩個字符串做減法的錯誤。字符串、map、slice和chan等所有的內置類型,都有嚴格的類型轉換規則。
|
||||||
|
|
||||||
對於無法靜態檢測到的錯誤, 例如數組訪問越界或使用空指針, 動態檢測可以保證程序在遇到問題的時候立卽終止併打印相關的錯誤信息. 自動內存管理(垃圾迴收)消除了大部分野指針和內存洩漏的問題.
|
對於無法靜態檢測到的錯誤,例如數組訪問越界或使用空指針,運行時動態檢測可以保證程序在遇到問題的時候立卽終止併打印相關的錯誤信息。自動內存管理(垃圾內存自動迴收)可以消除大部分野指針和內存洩漏相關的問題。
|
||||||
|
|
||||||
Go的實現刻意隱藏了很多底層細節. 我們無法知道一個結構體的內存布局, 也無法穫取一個運行函數的機器碼, 也無法知道當前的 goroutine 是運行在哪個操作繫統線程上. 事實上, Go的調度器會自己決定是否需要將 goroutine 從一個操作繫統線程轉移到另一個操作繫統線程. 一個指向變量的指針也併沒有展示變量眞實的地址. 因爲垃圾迴收器會根據需要移動變量的位置, 當然對應的也會被自動更新.
|
Go語言的實現刻意隱藏了很多底層細節。我們無法知道一個結構體眞實的內存布局,也無法穫取一個運行時函數對應的機器碼,也無法知道當前的goroutine是運行在哪個操作繫統線程之上。事實上,Go語言的調度器會自己決定是否需要將某個goroutine從一個操作繫統線程轉移到另一個操作繫統線程。一個指向變量的指針也併沒有展示變量眞實的地址。因爲垃圾迴收器可能會根據需要移動變量的內存位置,當然變量對應的地址也會被自動更新。
|
||||||
|
|
||||||
總的來説, Go語言的這些特殊使得Go程序相比較低級的C語言來説, 更容易預測, 更容易理解, 也不容易崩潰. 通過隱藏底層的細節, 也使得Go程序具有高度的可移植性, 因爲語言的語義在很大程度上是獨立於任何編譯器, 操作繫統和CPU繫統結構的(當然也不完全絶對獨立: 例如CPU字的大小, 某些表達式求值的順序, 還有編譯器實現的一些限製).
|
總的來説,Go語言的這些特性使得Go程序相比較低級的C語言來説更容易預測和理解,程序也不容易崩潰。通過隱藏底層的實現細節,也使得Go語言編寫的程序具有高度的可移植性,因爲語言的語義在很大程度上是獨立於任何編譯器實現、操作繫統和CPU繫統結構的(當然也不是完全絶對獨立:例如int等類型就依賴於CPU機器字的大小,某些表達式求值的具體順序,還有編譯器實現的一些額外的限製等)。
|
||||||
|
|
||||||
有時候我們可能會放棄部分語言特性而優先選擇更好的性能優化, 與其他語言編寫的庫互操作, 或者不用純Go語言來實現某些函數.
|
有時候我們可能會放棄使用部分語言特性而優先選擇更好具有更好性能的方法,例如需要與其他語言編寫的庫互操作,或者用純Go語言無法實現的某些函數。
|
||||||
|
|
||||||
在本章, 我們將展示如何使用 unsafe 包來襬脫通常的規則限製, 如何創建C函數庫的綁定, 以及如何進行繫統調用.
|
在本章,我們將展示如何使用unsafe包來襬脫Go語言規則帶來的限製,講述如何創建C語言函數庫的綁定,以及如何進行繫統調用。
|
||||||
|
|
||||||
本章描述的方法不應該輕易使用. 如果沒有處理好細節, 它們可能導致各種不可預測的隱晦的錯誤, 甚至連本地的C程序員也無法理解. 使用 unsafe 包同時也無法保證與未來版本的兼容性, 因爲在有意無意中會使用很多實現的細節, 而這些實現的細節在未來很可能會改變.
|
本章提供的方法不應該輕易使用(譯註:屬於黑魔法,雖然可能功能很強大,但是也容易誤傷到自己)。如果沒有處理好細節,它們可能導致各種不可預測的併且隱晦的錯誤,甚至連有經驗的的C語言程序員也無法理解這些錯誤。使用unsafe包的同時也放棄了Go語言保證與未來版本的兼容性的承諾,因爲它必然會在有意無意中會使用很多實現的細節,而這些實現的細節在未來的Go語言中很可能會被改變。
|
||||||
|
|
||||||
unsafe 包的實現比較特殊. 雖然它可以和普通包一樣的導入和使用, 但它實際上是由編譯器實現的. 它提供了一些訪問語言內部特性的方法, 特别是內存布局相關的細節.
|
要註意的是,unsafe包是一個采用特殊方式實現的包。雖然它可以和普通包一樣的導入和使用,但它實際上是由編譯器實現的。它提供了一些訪問語言內部特性的方法,特别是內存布局相關的細節。將這些特性封裝到一個獨立的包中,是爲在極少數情況下需要使用的時候,同時引起人們的註意(譯註:因爲看包的名字就知道使用unsafe包是不安全的)。此外,有一些環境因爲安全的因素可能限製這個包的使用。
|
||||||
將這些特别封裝到一個獨立的包中, 是爲在極少數情況下需要使用的時候, 引起人們的註意(它們是不安全的). 此外, 有一些環境因爲安全的因素可能限製這個包的使用.
|
|
||||||
|
|
||||||
unsafe 包被廣泛地用於比較低級的包, 例如 runtime, os, syscall 還有 net 等, 因爲它們需要和操作繫統密切配合的, 但是普通的程序一般是不需要的.
|
不過unsafe包被廣泛地用於比較低級的包, 例如runtime、os、syscall還有net包等,因爲它們需要和操作繫統密切配合,但是對於普通的程序一般是不需要使用unsafe包的。
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user