mirror of
https://github.com/gopl-zh/gopl-zh.github.com.git
synced 2025-12-17 19:24:19 +08:00
make loop
This commit is contained in:
@@ -1,19 +1,19 @@
|
||||
## 13.1. unsafe.Sizeof, Alignof 和 Offsetof
|
||||
|
||||
`unsafe.Sizeof` 函數返迴操作數在內存的字節大小, 可以是任意類型的表達式, 但是並不會對表達式進行求值. `Sizeof` 是一個 uintptr 類型的常量表達式, 因此返迴的結果可以用着數據的大小, 或者用作計算其他的常量.
|
||||
`unsafe.Sizeof` 函數返迴操作數在內存的字節大小, 可以是任意類型的表達式, 但是併不會對表達式進行求值. `Sizeof` 是一個 uintptr 類型的常量表達式, 因此返迴的結果可以用着數據的大小, 或者用作計算其他的常量.
|
||||
|
||||
```Go
|
||||
import "unsafe"
|
||||
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 字節對齊的. 但是對於再大的地址對齊倍數則是不需要的,
|
||||
例如 2 字節大小的 int16 類型應該是偶數, 一個4 字節大小的 rune 類型地址應該是 4 的倍數, 一個 8 字節大小的 float64, uint64 或 64-bit 指針 的地址應該是 8 字節對齊的. 但是對於再大的地址對齊倍數則是不需要的,
|
||||
卽使是 complex128 等較大的數據類型.
|
||||
|
||||
由於這個因素,一個聚合類型(結構體或數組)的大小至少是所有字段或元素大小的總和, 或者更大因爲可能存在空洞. 空洞是編譯器自動添加的沒有被使用的空間, 用於保証後面每個字段或元素的地址相對於結構或數組的開始地址能夠合理地對齊.
|
||||
由於這個因素,一個聚合類型(結構體或數組)的大小至少是所有字段或元素大小的總和, 或者更大因爲可能存在空洞. 空洞是編譯器自動添加的沒有被使用的空間, 用於保證後面每個字段或元素的地址相對於結構或數組的開始地址能夠合理地對齊.
|
||||
|
||||
|
||||
類型 | 大小
|
||||
@@ -29,7 +29,7 @@ func | 1個機器字
|
||||
chan | 1個機器字
|
||||
interface | 2個機器字(type,value)
|
||||
|
||||
Go的語言規範並沒有保証一個字段的聲明順序和內存中的順序是一緻的, 所以理論上一個編譯器可以隨意地重新排列每個字段的內存佈侷, 隨着在寫作本書的時候編譯器還沒有這麽做. 下面的三個結構體有着相同的字段, 但是第一個比另外的兩個需要多 50% 的內存.
|
||||
Go的語言規范併沒有保證一個字段的聲明順序和內存中的順序是一致的, 所以理論上一個編譯器可以隨意地重新排列每個字段的內存布局, 隨着在寫作本書的時候編譯器還沒有這麽做. 下面的三個結構體有着相同的字段, 但是第一個比另外的兩個需要多 50% 的內存.
|
||||
|
||||
|
||||
```Go
|
||||
@@ -39,9 +39,9 @@ struct{ float64; int16; bool } // 2 words 3words
|
||||
struct{ bool; int16; float64 } // 2 words 3words
|
||||
```
|
||||
|
||||
雖然關於對齊算法的細節超齣了本書的範圍, 也不是每一個結構體都需要擔心這個問題, 不過有效的包裝可以使數據結構更加緊湊, 內存使用率和性能都可能受益.
|
||||
雖然關於對齊算法的細節超齣了本書的范圍, 也不是每一個結構體都需要擔心這個問題, 不過有效的包裝可以使數據結構更加緊湊, 內存使用率和性能都可能受益.
|
||||
|
||||
`unsafe.Alignof` 函數返迴對應參數的類型需要對齊的倍數. 和 Sizeof 類似, Alignof 也是返迴一個常量表達式, 對應一個常量. 通常情況下佈爾和數字類型需要對齊到它們本身的大小(最多8個字節), 其它的類型對齊到機器字大小.
|
||||
`unsafe.Alignof` 函數返迴對應參數的類型需要對齊的倍數. 和 Sizeof 類似, Alignof 也是返迴一個常量表達式, 對應一個常量. 通常情況下布爾和數字類型需要對齊到它們本身的大小(最多8個字節), 其它的類型對齊到機器字大小.
|
||||
|
||||
`unsafe.Offsetof` 函數的參數必鬚是一個字段 `x.f`, 然後返迴 `f` 字段相對於 `x` 起始地址的偏移量, 包括可能的空洞.
|
||||
|
||||
@@ -80,6 +80,6 @@ Sizeof(x.b) = 2 Alignof(x.b) = 2 Offsetof(x.b) = 2
|
||||
Sizeof(x.c) = 24 Alignof(x.c) = 8 Offsetof(x.c) = 8
|
||||
```
|
||||
|
||||
雖然它們在不安全的 unsafe 包, 但是這幾個函數並不是眞的不安全,
|
||||
特別在需要優化內存空間時它們對於理解原生的內存佈侷很有幫助.
|
||||
雖然它們在不安全的 unsafe 包, 但是這幾個函數併不是眞的不安全,
|
||||
特别在需要優化內存空間時它們對於理解原生的內存布局很有幫助.
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
## 13.2. unsafe.Pointer
|
||||
|
||||
大多數指鍼類型寫成 *T, 含義是 "一個指向T類型變量的指鍼". `unsafe.Pointer` 是特別定義的一種指鍼類型, 它可以包含任意類型變量的地址. 當然, 我們不可以直接使用 *p 獲取 `unsafe.Pointer` 指鍼指向的眞實變量, 因爲我們並不知道變量的類型. 和普通指鍼一樣, `unsafe.Pointer` 指鍼是可以比較的, 支持和 nil 比較判斷是否爲空指鍼.
|
||||
大多數指針類型寫成 *T, 含義是 "一個指向T類型變量的指針". `unsafe.Pointer` 是特别定義的一種指針類型, 它可以包含任意類型變量的地址. 當然, 我們不可以直接使用 *p 穫取 `unsafe.Pointer` 指針指向的眞實變量, 因爲我們併不知道變量的類型. 和普通指針一樣, `unsafe.Pointer` 指針是可以比較的, 支持和 nil 比較判斷是否爲空指針.
|
||||
|
||||
一個普通的 *T 類型指鍼可以被轉化爲 `unsafe.Pointer` 類型指鍼, 並且一個 `unsafe.Pointer` 類型指鍼也可以被轉迴普通指鍼, 也可以是和 *T 不同類型的指鍼. 通過將 `*float64` 類型指鍼 轉化爲 `*uint64` 類型指鍼, 我們可以檢査一個浮點數變量的位模式.
|
||||
一個普通的 *T 類型指針可以被轉化爲 `unsafe.Pointer` 類型指針, 併且一個 `unsafe.Pointer` 類型指針也可以被轉迴普通指針, 也可以是和 *T 不同類型的指針. 通過將 `*float64` 類型指針 轉化爲 `*uint64` 類型指針, 我們可以檢査一個浮點數變量的位模式.
|
||||
|
||||
```Go
|
||||
package math
|
||||
@@ -12,13 +12,13 @@ func Float64bits(f float64) uint64 { return *(*uint64)(unsafe.Pointer(&f)) }
|
||||
fmt.Printf("%#016x\n", Float64bits(1.0)) // "0x3ff0000000000000"
|
||||
```
|
||||
|
||||
通過新指鍼, 我們可以更新浮點數的位模式. 通過位模式操作浮點數是可以的, 但是更重要的意義是指鍼轉換讓我們可以在不破壞類型繫統的前提下向內存寫入任意的值.
|
||||
通過新指針, 我們可以更新浮點數的位模式. 通過位模式操作浮點數是可以的, 但是更重要的意義是指針轉換讓我們可以在不破壞類型繫統的前提下向內存寫入任意的值.
|
||||
|
||||
一個 `unsafe.Pointer` 指鍼也可以被轉化爲 uintptr 類似, 然後保存到指鍼型數值變量中, 用以做必要的指鍼運算.
|
||||
一個 `unsafe.Pointer` 指針也可以被轉化爲 uintptr 類似, 然後保存到指針型數值變量中, 用以做必要的指針運算.
|
||||
(第三章內容, uintptr是一個無符號的整型數, 足有保存一個地址.)
|
||||
這種轉換也是可逆的, 但是, 將 uintptr 轉爲 `unsafe.Pointer` 指鍼可能破壞類型繫統, 因爲並不是所有的數字都是有效的內存地址.
|
||||
這種轉換也是可逆的, 但是, 將 uintptr 轉爲 `unsafe.Pointer` 指針可能破壞類型繫統, 因爲併不是所有的數字都是有效的內存地址.
|
||||
|
||||
許多將 `unsafe.Pointer` 指鍼 轉爲原生數字, 然後再轉爲 `unsafe.Pointer` 指鍼的操作是不安全的. 下面的例子需要將變量 x 的地址加上 b 字段的偏移轉化爲 *int16 類型指鍼, 然後通過該指鍼更新 `x.b`:
|
||||
許多將 `unsafe.Pointer` 指針 轉爲原生數字, 然後再轉爲 `unsafe.Pointer` 指針的操作是不安全的. 下面的例子需要將變量 x 的地址加上 b 字段的偏移轉化爲 *int16 類型指針, 然後通過該指針更新 `x.b`:
|
||||
|
||||
```Go
|
||||
//gopl.io/ch13/unsafeptr
|
||||
@@ -36,24 +36,24 @@ pb := (*int16)(unsafe.Pointer(
|
||||
fmt.Println(x.b) // "42"
|
||||
```
|
||||
|
||||
儘管寫法很繁瑣, 但在這裡並不是一件壞事, 因爲這些功能應該很謹慎地使用. 不要試圖將引入可能而破壞代碼的正確性的 uintptr 臨時變量. 下面段代碼是不正確的:
|
||||
盡管寫法很繁瑣, 但在這里併不是一件壞事, 因爲這些功能應該很謹慎地使用. 不要試圖將引入可能而破壞代碼的正確性的 uintptr 臨時變量. 下面段代碼是不正確的:
|
||||
|
||||
錯誤的原因很微妙. 有時候垃圾迴收器會移動一些變量以降低內存碎片的問題.這類垃圾迴收器被稱爲移動GC. 當一個變量被移動, 所有的保存改變量舊地址的指鍼必鬚同時被更新爲變量移動後的新地址. 從垃圾收集器的視角來看, 一個 `unsafe.Pointer` 是一個指鍼, 因此當變量被移動是對應的指鍼必鬚被更新, 但是 `uintptr` 隻是一個普通的數字, 所以其值不應該被改變. 上面錯誤的代碼因爲一個非指鍼的臨時變量 `tmp`, 導緻垃圾收集器無法正確識別這個是一個指向變量 `x` 的指鍼. 第二個語句執行時, 變量 `x` 可能已經被轉移, 臨時變量 `tmp` 也就不在對應現在的 `&x.b`. 第三個賦值語句將徹底摧毀那個之前的那部分內存空間.
|
||||
錯誤的原因很微妙. 有時候垃圾迴收器會移動一些變量以降低內存碎片的問題.這類垃圾迴收器被稱爲移動GC. 當一個變量被移動, 所有的保存改變量舊地址的指針必鬚同時被更新爲變量移動後的新地址. 從垃圾收集器的視角來看, 一個 `unsafe.Pointer` 是一個指針, 因此當變量被移動是對應的指針必鬚被更新, 但是 `uintptr` 隻是一個普通的數字, 所以其值不應該被改變. 上面錯誤的代碼因爲一個非指針的臨時變量 `tmp`, 導致垃圾收集器無法正確識别這個是一個指向變量 `x` 的指針. 第二個語句執行時, 變量 `x` 可能已經被轉移, 臨時變量 `tmp` 也就不在對應現在的 `&x.b`. 第三個賦值語句將徹底摧譭那個之前的那部分內存空間.
|
||||
|
||||
有很多類似原因導緻的錯誤. 例如這條語句:
|
||||
有很多類似原因導致的錯誤. 例如這條語句:
|
||||
|
||||
```Go
|
||||
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). 譯註: 該問題已經脩複.), 因此我們強烈建議按照最壞的方式處理. 將所有包含變量 `y` 地址的 `uintptr` 類型變量當作 BUG 處理, 同時減少不必要的 `unsafe.Pointer` 到 `uintptr` 的轉換. 在第一個例子中, 有三個到 `uintptr` 的轉換, 字段偏移量的運算, 所有的轉換全在一個表達式完成.
|
||||
|
||||
當調用一個庫函數, 並且返迴的是 `uintptr` 類型是, 比如下面反射包中的相關函數,
|
||||
返迴的結果應該立卽轉換爲 `unsafe.Pointer` 以確保指鍼指向的是相同的變量.
|
||||
當調用一個庫函數, 併且返迴的是 `uintptr` 類型是, 比如下面反射包中的相關函數,
|
||||
返迴的結果應該立卽轉換爲 `unsafe.Pointer` 以確保指針指向的是相同的變量.
|
||||
|
||||
```Go
|
||||
package reflect
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
## 13.3. 示例: 深度相等判斷
|
||||
|
||||
來自 reflect 包的 DeepEqual 對兩個值進行深度相等判斷. DeepEqual 使用內建的 `==` 操作符對基礎類型進行相等判斷, 對於復合類型則遞歸變量每個基礎類型然後做類似的比較判斷. 因爲它工作在任意的類型上, 甚至對一些不支持 `==` 操作符的類型也可以工作, 因此在一些測試代碼中被廣汎地使用. 比如下面的代碼是用 DeepEqual 比較兩個字符串數組是否等價.
|
||||
來自 reflect 包的 DeepEqual 對兩個值進行深度相等判斷. DeepEqual 使用內建的 `==` 操作符對基礎類型進行相等判斷, 對於複合類型則遞歸變量每個基礎類型然後做類似的比較判斷. 因爲它工作在任意的類型上, 甚至對一些不支持 `==` 操作符的類型也可以工作, 因此在一些測試代碼中被廣泛地使用. 比如下面的代碼是用 DeepEqual 比較兩個字符串數組是否等價.
|
||||
|
||||
```Go
|
||||
func TestSplit(t *testing.T) {
|
||||
@@ -10,7 +10,7 @@ func TestSplit(t *testing.T) {
|
||||
}
|
||||
```
|
||||
|
||||
儘管 DeepEqual 很方便, 而且可以支持任意的類型, 但是也有不足之處.
|
||||
盡管 DeepEqual 很方便, 而且可以支持任意的類型, 但是也有不足之處.
|
||||
例如, 它將一個 nil map 和 非 nil 的空的 map 視作不相等,
|
||||
同樣 nil slice 和 非 nil 的空的 slice 也不相等.
|
||||
|
||||
@@ -22,7 +22,7 @@ var c, d map[string]int = nil, make(map[string]int)
|
||||
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 視作相等的值. 基礎部分的比較可以基於反射完成, 和 12.3 章的 Display 實現方法類似. 同樣, 我們頂一個一個內部函數 equal, 用於內部的遞歸比較. 目前不用關心 seen 參數. 對於每一對需要比較的 x 和 y, equal 函數 首先檢測它們是否都有效(或都無效), 然後檢測它們是否是相同的類型. 剩下的部分是一個大的 switch 分支, 用於擁有相同基礎類型的比較. 因爲頁面空間的限製, 我們省略了一些類似的分支.
|
||||
|
||||
```Go
|
||||
gopl.io/ch13/equal
|
||||
@@ -108,7 +108,7 @@ fmt.Println(Equal([]string(nil), []string{})) // "true"
|
||||
fmt.Println(Equal(map[string]int(nil), map[string]int{})) // "true"
|
||||
```
|
||||
|
||||
它甚至可以處理類似12.3章中導緻Display陷入死循環的數據.
|
||||
它甚至可以處理類似12.3章中導致Display陷入死循環的數據.
|
||||
|
||||
```Go
|
||||
// Circular linked lists a -> b -> a and c -> c.
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
## 13.4. 通過cgo調用C代碼
|
||||
|
||||
Go程序可能會遇到要訪問C語言的某些硬件驅動的場景, 或者是從一個C++實現的嵌入式數據庫査詢記録的場景, 或者是使用Fortran實現的一些綫性代數庫的場景. C作爲一個通用語言, 很多庫會選擇提供一個C兼容的API, 然後用其他語言實現.
|
||||
Go程序可能會遇到要訪問C語言的某些硬件驅動的場景, 或者是從一個C++實現的嵌入式數據庫査詢記録的場景, 或者是使用Fortran實現的一些線性代數庫的場景. C作爲一個通用語言, 很多庫會選擇提供一個C兼容的API, 然後用其他語言實現.
|
||||
|
||||
在本節中, 我們將構建一個簡易的數據壓縮程序, 通過使用一個Go語言自帶的叫cgo的用於支援C語言函數調用的工具. 這類工具被稱爲外圍函數接口(ffi), 並且cgo也不是Go中唯一的類似工具. SWIG(swig.org) 是類似的另一個被廣汎使用的工具, 它提供了很多復雜特性以支援C++的集成, 但 SWIG 不是這裡要討論的主題.
|
||||
在本節中, 我們將構建一個簡易的數據壓縮程序, 通過使用一個Go語言自帶的叫cgo的用於支援C語言函數調用的工具. 這類工具被稱爲外圍函數接口(ffi), 併且cgo也不是Go中唯一的類似工具. SWIG(swig.org) 是類似的另一個被廣泛使用的工具, 它提供了很多複雜特性以支援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
|
||||
package gzip // compress/gzip
|
||||
@@ -14,7 +14,7 @@ func NewReader(r io.Reader) (io.ReadCloser, error)
|
||||
|
||||
bzip2壓縮算法, 是基於優雅的 Burrows-Wheeler 變換, 運行速度比 gzip 要慢, 但是可以提供更高的壓縮比. 標準庫的 `compress/bzip2` 包目前還沒有提供 bzip2 算法的壓縮實現. 完全從頭實現是一個繁瑣的工作, 而且 bzip.org 有現成的 libbzip2 開源實現, 文檔齊全而且性能較好,
|
||||
|
||||
如果C庫比較小, 我們可以用純Go重新實現一遍. 如果我們對性能沒有特殊要求, 我們可以用 `os/exec` 包的方法將C編寫的應用程序作爲一個子進行運行. 隻有當你需要使用復雜但是性能更高的底層C接口時, 就是使用cgo的場景了. 下面我們將通過一個例子講述cgo的用法.
|
||||
如果C庫比較小, 我們可以用純Go重新實現一遍. 如果我們對性能沒有特殊要求, 我們可以用 `os/exec` 包的方法將C編寫的應用程序作爲一個子進行運行. 隻有當你需要使用複雜但是性能更高的底層C接口時, 就是使用cgo的場景了. 下面我們將通過一個例子講述cgo的用法.
|
||||
|
||||
要使用 libbzip2, 我們需要一個 `bz_stream` 結構體, 用於保持輸入和輸齣緩存.
|
||||
然後有三個函數: BZ2_bzCompressInit 用於初始化緩存, BZ2_bzCompress 用於將輸入緩存的數據壓縮到輸齣緩存, BZ2_bzCompressEnd 用於釋放不需要的緩存.
|
||||
@@ -42,7 +42,7 @@ int bz2compress(bz_stream *s, int action,
|
||||
}
|
||||
```
|
||||
|
||||
現在讓我們轉到Go部分, 第一部分如下所示. 其中 `import "C"` 的語句是比較特別的. 其實並沒有一個叫 `C` 的包, 但是這行語句會讓Go構建在編譯之前先運行cgo工具.
|
||||
現在讓我們轉到Go部分, 第一部分如下所示. 其中 `import "C"` 的語句是比較特别的. 其實併沒有一個叫 `C` 的包, 但是這行語句會讓Go構建在編譯之前先運行cgo工具.
|
||||
|
||||
|
||||
```Go
|
||||
@@ -82,7 +82,7 @@ func NewWriter(out io.Writer) io.WriteCloser {
|
||||
}
|
||||
```
|
||||
|
||||
在循環的每次迭代中, 向bz2compress傳入數據的地址和剩餘部分的長度, 還有輸齣緩存 w.outbuf 的地址和容量. 這兩個長度信息通過它們的地址傳入而不是值傳入, 因爲bz2compress函數可能會根據已經壓縮的數據和壓縮後數據的大小來更新這兩個值(譯註: 這裡的用法有問題, 勘誤已經提到. 具體脩復的方法稍後再補充). 每個塊壓縮後的數據被寫入到底層的 io.Writer.
|
||||
在循環的每次迭代中, 向bz2compress傳入數據的地址和剩餘部分的長度, 還有輸齣緩存 w.outbuf 的地址和容量. 這兩個長度信息通過它們的地址傳入而不是值傳入, 因爲bz2compress函數可能會根據已經壓縮的數據和壓縮後數據的大小來更新這兩個值(譯註: 這里的用法有問題, 勘誤已經提到. 具體脩複的方法稍後再補充). 每個塊壓縮後的數據被寫入到底層的 io.Writer.
|
||||
|
||||
Close 方法和 Write 方法有着類似的結構, 通過一個循環將剩餘的壓縮數據刷新到輸齣緩存.
|
||||
|
||||
@@ -111,9 +111,9 @@ func (w *writer) Close() error {
|
||||
}
|
||||
```
|
||||
|
||||
壓縮完成後, Close 用了 defer 確保函數退齣前調用 C.BZ2_bzCompressEnd 釋放輸入和輸齣流的緩存. 此刻 `w.stream` 指鍼將不在有效, 我們將它設置爲 nil 以保証安全, 然後在每個方法中增加 nil 檢測, 以防止用戶在關閉後依然錯誤使用相關方法.
|
||||
壓縮完成後, Close 用了 defer 確保函數退齣前調用 C.BZ2_bzCompressEnd 釋放輸入和輸齣流的緩存. 此刻 `w.stream` 指針將不在有效, 我們將它設置爲 nil 以保證安全, 然後在每個方法中增加 nil 檢測, 以防止用戶在關閉後依然錯誤使用相關方法.
|
||||
|
||||
不僅僅寫是非並發安全的, 甚至並發調用 Close 和 Write 也可能導緻C代碼的崩潰. 脩復這個問題是 練習13.3 的內容.
|
||||
不僅僅寫是非併發安全的, 甚至併發調用 Close 和 Write 也可能導致C代碼的崩潰. 脩複這個問題是 練習13.3 的內容.
|
||||
|
||||
下面的bzipper程序是使用我們自己包實現的bzip2壓縮命令. 它的行爲和許多Unix繫統的 bzip2 命令類似.
|
||||
|
||||
@@ -141,7 +141,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
|
||||
@@ -155,8 +155,8 @@ $ ./bzipper < /usr/share/dict/words | bunzip2 | sha256sum
|
||||
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程序中動態加載也都是可行的. 這里我們隻展示的cgo很小的一些方面, 更多的關於內存管理, 指針, 迴調函數, 信號處理, 字符串, errno處理, 終結器, 以及 goroutines 和繫統線程的關繫等, 有很多細節可以討論. 特别是如何將Go的指針傳入C函數的規則也是異常複雜的, 部分的原因在 13.2節 有討論到, 但是在Go1.5中還沒有被明確. 如果要進一步閲讀, 可以從 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 的替代實現.
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
我們在前一章結尾的時候, 我們警告要謹慎使用反射. 那些警告同樣適用於本章的 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包, 除非你確實需要它們.
|
||||
|
||||
|
||||
18
ch13/ch13.md
18
ch13/ch13.md
@@ -1,21 +1,21 @@
|
||||
# 第13章 底層編程
|
||||
|
||||
Go的設計包含了諸多安全策略, 限製了可能導緻程序錯誤的用法. 編譯時類型檢査檢測可以發現大多數類型不匹配的變量操作, 例如兩個字符串做減法的錯誤. 字符串, 字典, 切片 和管道等所有的內置類型, 都有嚴格的類型轉換規則.
|
||||
Go的設計包含了諸多安全策略, 限製了可能導致程序錯誤的用法. 編譯時類型檢査檢測可以發現大多數類型不匹配的變量操作, 例如兩個字符串做減法的錯誤. 字符串, 字典, 切片 和管道等所有的內置類型, 都有嚴格的類型轉換規則.
|
||||
|
||||
對於無法靜態檢測到的錯誤, 例如數組訪問越界或使用空指鍼, 動態檢測可以保証程序在遇到問題的時候立卽終止並打印相關的錯誤信息. 自動內存管理(垃圾迴收)消除了大部分野指鍼和內存洩漏的問題.
|
||||
對於無法靜態檢測到的錯誤, 例如數組訪問越界或使用空指針, 動態檢測可以保證程序在遇到問題的時候立卽終止併打印相關的錯誤信息. 自動內存管理(垃圾迴收)消除了大部分野指針和內存洩漏的問題.
|
||||
|
||||
Go的實現刻意隱藏了很多底層細節. 我們無法知道一個結構體的內存佈侷, 也無法獲取一個運行函數的機器碼, 也無法知道當前的 goroutine 是運行在哪個操作繫統綫程上. 事實上, Go的調度器會自己決定是否需要將 goroutine 從一個操作繫統綫程轉移到另一個操作繫統綫程. 一個指向變量的指鍼也並沒有展示變量眞實的地址. 因爲垃圾迴收器會根據需要移動變量的位置, 當然對應的也會被自動更新.
|
||||
Go的實現刻意隱藏了很多底層細節. 我們無法知道一個結構體的內存布局, 也無法穫取一個運行函數的機器碼, 也無法知道當前的 goroutine 是運行在哪個操作繫統線程上. 事實上, Go的調度器會自己決定是否需要將 goroutine 從一個操作繫統線程轉移到另一個操作繫統線程. 一個指向變量的指針也併沒有展示變量眞實的地址. 因爲垃圾迴收器會根據需要移動變量的位置, 當然對應的也會被自動更新.
|
||||
|
||||
總的來說, Go語言的這些特殊使得Go程序相比較低級的C語言來說, 更容易預測, 更容易理解, 也不容易崩潰. 通過隱藏底層的細節, 也使得Go程序具有高度的可移植性, 因爲語言的語義在很大程度上是獨立於任何編譯器, 操作繫統和CPU繫統結構的(當然也不完全絕對獨立: 例如CPU字的大小, 某些表達式求值的順序, 還有編譯器實現的一些限製).
|
||||
總的來説, Go語言的這些特殊使得Go程序相比較低級的C語言來説, 更容易預測, 更容易理解, 也不容易崩潰. 通過隱藏底層的細節, 也使得Go程序具有高度的可移植性, 因爲語言的語義在很大程度上是獨立於任何編譯器, 操作繫統和CPU繫統結構的(當然也不完全絶對獨立: 例如CPU字的大小, 某些表達式求值的順序, 還有編譯器實現的一些限製).
|
||||
|
||||
有時候我們可能會放棄部分語言特性而優先選擇更好的性能優化, 與其他語言編寫的庫互操作, 或者不用純Go語言來實現某些函數.
|
||||
|
||||
在本章, 我們將展示如何使用 unsafe 包來擺脫通常的規則限製, 如何創建C函數庫的綁定, 以及如何進行繫統調用.
|
||||
在本章, 我們將展示如何使用 unsafe 包來襬脫通常的規則限製, 如何創建C函數庫的綁定, 以及如何進行繫統調用.
|
||||
|
||||
本章描述的方法不應該輕易使用. 如果沒有處理好細節, 它們可能導緻各種不可預測的隱晦的錯誤, 甚至連本地的C程序員也無法理解. 使用 unsafe 包同時也無法保証與未來版本的兼容性, 因爲在有意無意中會使用很多實現的細節, 而這些實現的細節在未來很可能會改變.
|
||||
本章描述的方法不應該輕易使用. 如果沒有處理好細節, 它們可能導致各種不可預測的隱晦的錯誤, 甚至連本地的C程序員也無法理解. 使用 unsafe 包同時也無法保證與未來版本的兼容性, 因爲在有意無意中會使用很多實現的細節, 而這些實現的細節在未來很可能會改變.
|
||||
|
||||
unsafe 包的實現比較特殊. 雖然它可以和普通包一樣的導入和使用, 但它實際上是由編譯器實現的. 它提供了一些訪問語言內部特性的方法, 特別是內存佈侷相關的細節.
|
||||
將這些特別封裝到一個獨立的包中, 是爲在極少數情況下需要使用的時候, 引起人們的註意(它們是不安全的). 此外, 有一些環境因爲安全的因素可能限製這個包的使用.
|
||||
unsafe 包的實現比較特殊. 雖然它可以和普通包一樣的導入和使用, 但它實際上是由編譯器實現的. 它提供了一些訪問語言內部特性的方法, 特别是內存布局相關的細節.
|
||||
將這些特别封裝到一個獨立的包中, 是爲在極少數情況下需要使用的時候, 引起人們的註意(它們是不安全的). 此外, 有一些環境因爲安全的因素可能限製這個包的使用.
|
||||
|
||||
unsafe 包被廣汎地用於比較低級的包, 例如 runtime, os, syscall 還有 net 等, 因爲它們需要和操作繫統密切配合的, 但是普通的程序一般是不需要的.
|
||||
unsafe 包被廣泛地用於比較低級的包, 例如 runtime, os, syscall 還有 net 等, 因爲它們需要和操作繫統密切配合的, 但是普通的程序一般是不需要的.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user