# 第13章 底層編程
Go語言的設計包含了諸多安全策略,限制了可能導致程序運行出現錯誤的用法。編譯時類型檢查檢查可以發現大多數類型不匹配的操作,例如兩個字符串做減法的錯誤。字符串、map、slice和chan等所有的內置類型,都有嚴格的類型轉換規則。
對于無法靜態檢測到的錯誤,例如數組訪問越界或使用空指針,運行時動態檢測可以保證程序在遇到問題的時候立即終止并打印相關的錯誤信息。自動內存管理(垃圾內存自動回收)可以消除大部分野指針和內存泄漏相關的問題。
Go語言的實現刻意隱藏了很多底層細節。我們無法知道一個結構體真實的內存布局,也無法獲取一個運行時函數對應的機器碼,也無法知道當前的goroutine是運行在哪個操作系統線程之上。事實上,Go語言的調度器會自己決定是否需要將某個goroutine從一個操作系統線程轉移到另一個操作系統線程。一個指向變量的指針也并沒有展示變量真實的地址。因為垃圾回收器可能會根據需要移動變量的內存位置,當然變量對應的地址也會被自動更新。
總的來說,Go語言的這些特性使得Go程序相比較低級的C語言來說更容易預測和理解,程序也不容易崩潰。通過隱藏底層的實現細節,也使得Go語言編寫的程序具有高度的可移植性,因為語言的語義在很大程度上是獨立于任何編譯器實現、操作系統和CPU系統結構的(當然也不是完全絕對獨立:例如int等類型就依賴于CPU機器字的大小,某些表達式求值的具體順序,還有編譯器實現的一些額外的限制等)。
有時候我們可能會放棄使用部分語言特性而優先選擇更好具有更好性能的方法,例如需要與其他語言編寫的庫互操作,或者用純Go語言無法實現的某些函數。
在本章,我們將展示如何使用unsafe包來擺脫Go語言規則帶來的限制,講述如何創建C語言函數庫的綁定,以及如何進行系統調用。
本章提供的方法不應該輕易使用(譯注:屬于黑魔法,雖然可能功能很強大,但是也容易誤傷到自己)。如果沒有處理好細節,它們可能導致各種不可預測的并且隱晦的錯誤,甚至連有經驗的的C語言程序員也無法理解這些錯誤。使用unsafe包的同時也放棄了Go語言保證與未來版本的兼容性的承諾,因為它必然會在有意無意中會使用很多實現的細節,而這些實現的細節在未來的Go語言中很可能會被改變。
要注意的是,unsafe包是一個采用特殊方式實現的包。雖然它可以和普通包一樣的導入和使用,但它實際上是由編譯器實現的。它提供了一些訪問語言內部特性的方法,特別是內存布局相關的細節。將這些特性封裝到一個獨立的包中,是為在極少數情況下需要使用的時候,同時引起人們的注意(譯注:因為看包的名字就知道使用unsafe包是不安全的)。此外,有一些環境因為安全的因素可能限制這個包的使用。
不過unsafe包被廣泛地用于比較低級的包, 例如runtime、os、syscall還有net包等,因為它們需要和操作系統密切配合,但是對于普通的程序一般是不需要使用unsafe包的。
### 13.1. unsafe.Sizeof, Alignof 和 Offsetof
unsafe.Sizeof函數返回操作數在內存中的字節大小,參數可以是任意類型的表達式,但是它并不會對表達式進行求值。一個Sizeof函數調用是一個對應uintptr類型的常量表達式,因此返回的結果可以用作數組類型的長度大小,或者用作計算其他的常量。
~~~
import "unsafe"
fmt.Println(unsafe.Sizeof(float64(0))) // "8"
~~~
Sizeof函數返回的大小只包括數據結構中固定的部分,例如字符串對應結構體中的指針和字符串長度部分,但是并不包含指針指向的字符串的內容。Go語言中非聚合類型通常有一個固定的大小,盡管在不同工具鏈下生成的實際大小可能會有所不同。考慮到可移植性,引用類型或包含引用類型的大小在32位平臺上是4個字節,在64位平臺上是8個字節。
計算機在加載和保存數據時,如果內存地址合理地對齊的將會更有效率。例如2字節大小的int16類型的變量地址應該是偶數,一個4字節大小的rune類型變量的地址應該是4的倍數,一個8字節大小的float64、uint64或64-bit指針類型變量的地址應該是8字節對齊的。但是對于再大的地址對齊倍數則是不需要的,即使是complex128等較大的數據類型最多也只是8字節對齊。
由于地址對齊這個因素,一個聚合類型(結構體或數組)的大小至少是所有字段或元素大小的總和,或者更大因為可能存在內存空洞。內存空洞是編譯器自動添加的沒有被使用的內存空間,用于保證后面每個字段或元素的地址相對于結構或數組的開始地址能夠合理地對齊(譯注:內存空洞可能會存在一些隨機數據,可能會對用unsafe包直接操作內存的處理產生影響)。
類型 | 大小 ----------------------------- | ---- bool | 1個字節 intN, uintN, floatN, complexN | N/8個字節(例如float64是8個字節) int, uint, uintptr | 1個機器字 *T | 1個機器字 string | 2個機器字(data,len) []T | 3個機器字(data,len,cap) map | 1個機器字 func | 1個機器字 chan | 1個機器字 interface | 2個機器字(type,value)
Go語言的規范并沒有要求一個字段的聲明順序和內存中的順序是一致的,所以理論上一個編譯器可以隨意地重新排列每個字段的內存位置,隨然在寫作本書的時候編譯器還沒有這么做。下面的三個結構體雖然有著相同的字段,但是第一種寫法比另外的兩個需要多50%的內存。
~~~
// 64-bit 32-bit
struct{ bool; float64; int16 } // 3 words 4words
struct{ float64; int16; bool } // 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.Offsetof` 函數的參數必須是一個字段 `x.f`, 然后返回 `f` 字段相對于 `x` 起始地址的偏移量, 包括可能的空洞.
圖 13.1 顯示了一個結構體變量 x 以及其在32位和64位機器上的典型的內存. 灰色區域是空洞.
~~~
var x struct {
a bool
b int16
c []int
}
~~~
下面顯示了對x和它的三個字段調用unsafe包相關函數的計算結果:

32位系統:
~~~
Sizeof(x) = 16 Alignof(x) = 4
Sizeof(x.a) = 1 Alignof(x.a) = 1 Offsetof(x.a) = 0
Sizeof(x.b) = 2 Alignof(x.b) = 2 Offsetof(x.b) = 2
Sizeof(x.c) = 12 Alignof(x.c) = 4 Offsetof(x.c) = 4
~~~
64位系統:
~~~
Sizeof(x) = 32 Alignof(x) = 8
Sizeof(x.a) = 1 Alignof(x.a) = 1 Offsetof(x.a) = 0
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包,但是這幾個函數調用并不是真的不安全,特別在需要優化內存空間時它們返回的結果對于理解原生的內存布局很有幫助。
### 13.2. unsafe.Pointer
大多數指針類型會寫成`*T`,表示是“一個指向T類型變量的指針”。unsafe.Pointer是特別定義的一種指針類型(譯注:類似C語言中的`void*`類型的指針),它可以包含任意類型變量的地址。當然,我們不可以直接通過`*p`來獲取unsafe.Pointer指針指向的真實變量的值,因為我們并不知道變量的具體類型。和普通指針一樣,unsafe.Pointer指針也是可以比較的,并且支持和nil常量比較判斷是否為空指針。
一個普通的`*T`類型指針可以被轉化為unsafe.Pointer類型指針,并且一個unsafe.Pointer類型指針也可以被轉回普通的指針,被轉回普通的指針類型并不需要和原始的`*T`類型相同。通過將`*float64`類型指針轉化為`*uint64`類型指針,我們可以查看一個浮點數變量的位模式。
~~~
package math
func Float64bits(f float64) uint64 { return *(*uint64)(unsafe.Pointer(&f)) }
fmt.Printf("%#016x\n", Float64bits(1.0)) // "0x3ff0000000000000"
~~~
通過轉為新類型指針,我們可以更新浮點數的位模式。通過位模式操作浮點數是可以的,但是更重要的意義是指針轉換語法讓我們可以在不破壞類型系統的前提下向內存寫入任意的值。
一個unsafe.Pointer指針也可以被轉化為uintptr類型,然后保存到指針型數值變量中(譯注:這只是和當前指針相同的一個數字值,并不是一個指針),然后用以做必要的指針數值運算。(第三章內容,uintptr是一個無符號的整型數,足以保存一個地址)這種轉換雖然也是可逆的,但是將uintptr轉為unsafe.Pointer指針可能會破壞類型系統,因為并不是所有的數字都是有效的內存地址。
許多將unsafe.Pointer指針轉為原生數字,然后再轉回為unsafe.Pointer類型指針的操作也是不安全的。比如下面的例子需要將變量x的地址加上b字段地址偏移量轉化為`*int16`類型指針,然后通過該指針更新x.b:
*gopl.io/ch13/unsafeptr*
~~~
var x struct {
a bool
b int16
c []int
}
// 和 pb := &x.b 等價
pb := (*int16)(unsafe.Pointer(
uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b)))
*pb = 42
fmt.Println(x.b) // "42"
~~~
上面的寫法盡管很繁瑣,但在這里并不是一件壞事,因為這些功能應該很謹慎地使用。不要試圖引入一個uintptr類型的臨時變量,因為它可能會破壞代碼的安全性(譯注:這是真正可以體會unsafe包為何不安全的例子)。下面段代碼是錯誤的:
~~~
// 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`地址。第三個向之前無效地址空間的賦值語句將徹底摧毀整個程序!
還有很多類似原因導致的錯誤。例如這條語句:
~~~
pT := uintptr(unsafe.Pointer(new(T))) // 提示: 錯誤!
~~~
這里并沒有指針引用`new`新創建的變量,因此該語句執行完成之后,垃圾收集器有權馬上回收其內存空間,所以返回的pT將是無效的地址。
雖然目前的Go語言實現還沒有使用移動GC(譯注:未來可能實現),但這不該是編寫錯誤代碼僥幸的理由:當前的Go語言實現已經有移動變量的場景。在5.2節我們提到goroutine的棧是根據需要動態增長的。當發送棧動態增長的時候,原來棧中的所以變量可能需要被移動到新的更大的棧中,所以我們并不能確保變量的地址在整個使用周期內是不變的。
在編寫本文時,還沒有清晰的原則來指引Go程序員,什么樣的unsafe.Pointer和uintptr的轉換是不安全的(參考 [Issue7192](https://github.com/golang/go/issues/7192) ). 譯注: 該問題已經關閉),因此我們強烈建議按照最壞的方式處理。將所有包含變量地址的uintptr類型變量當作BUG處理,同時減少不必要的unsafe.Pointer類型到uintptr類型的轉換。在第一個例子中,有三個轉換——字段偏移量到uintptr的轉換和轉回unsafe.Pointer類型的操作——所有的轉換全在一個表達式完成。
當調用一個庫函數,并且返回的是uintptr類型地址時(譯注:普通方法實現的函數不盡量不要返回該類型。下面例子是reflect包的函數,reflect包和unsafe包一樣都是采用特殊技術實現的,編譯器可能給它們開了后門),比如下面反射包中的相關函數,返回的結果應該立即轉換為unsafe.Pointer以確保指針指向的是相同的變量。
~~~
package reflect
func (Value) Pointer() uintptr
func (Value) UnsafeAddr() uintptr
func (Value) InterfaceData() [2]uintptr // (index 1)
~~~
### 13.3. 示例: 深度相等判斷
來自reflect包的DeepEqual函數可以對兩個值進行深度相等判斷。DeepEqual函數使用內建的==比較操作符對基礎類型進行相等判斷,對于復合類型則遞歸該變量的每個基礎類型然后做類似的比較判斷。因為它可以工作在任意的類型上,甚至對于一些不支持==操作運算符的類型也可以工作,因此在一些測試代碼中廣泛地使用該函數。比如下面的代碼是用DeepEqual函數比較兩個字符串數組是否相等。
~~~
func TestSplit(t *testing.T) {
got := strings.Split("a:b:c", ":")
want := []string{"a", "b", "c"};
if !reflect.DeepEqual(got, want) { /* ... */ }
}
~~~
盡管DeepEqual函數很方便,而且可以支持任意的數據類型,但是它也有不足之處。例如,它將一個nil值的map和非nil值但是空的map視作不相等,同樣nil值的slice 和非nil但是空的slice也視作不相等。
~~~
var a, b []string = nil, []string{}
fmt.Println(reflect.DeepEqual(a, b)) // "false"
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視作相等的值。基礎部分的比較可以基于reflect包完成,和12.3章的Display函數的實現方法類似。同樣,我們也定義了一個內部函數equal,用于內部的遞歸比較。讀者目前不用關心seen參數的具體含義。對于每一對需要比較的x和y,equal函數首先檢測它們是否都有效(或都無效),然后檢測它們是否是相同的類型。剩下的部分是一個巨大的switch分支,用于相同基礎類型的元素比較。因為頁面空間的限制,我們省略了一些相似的分支。
*gopl.io/ch13/equal*
~~~
func equal(x, y reflect.Value, seen map[comparison]bool) bool {
if !x.IsValid() || !y.IsValid() {
return x.IsValid() == y.IsValid()
}
if x.Type() != y.Type() {
return false
}
// ...cycle check omitted (shown later)...
switch x.Kind() {
case reflect.Bool:
return x.Bool() == y.Bool()
case reflect.String:
return x.String() == y.String()
// ...numeric cases omitted for brevity...
case reflect.Chan, reflect.UnsafePointer, reflect.Func:
return x.Pointer() == y.Pointer()
case reflect.Ptr, reflect.Interface:
return equal(x.Elem(), y.Elem(), seen)
case reflect.Array, reflect.Slice:
if x.Len() != y.Len() {
return false
}
for i := 0; i < x.Len(); i++ {
if !equal(x.Index(i), y.Index(i), seen) {
return false
}
}
return true
// ...struct and map cases omitted for brevity...
}
panic("unreachable")
}
~~~
和前面的建議一樣,我們并不公開reflect包相關的接口,所以導出的函數需要在內部自己將變量轉為reflect.Value類型。
~~~
// Equal reports whether x and y are deeply equal.
func Equal(x, y interface{}) bool {
seen := make(map[comparison]bool)
return equal(reflect.ValueOf(x), reflect.ValueOf(y), seen)
}
type comparison struct {
x, y unsafe.Pointer
treflect.Type
}
~~~
為了確保算法對于有環的數據結構也能正常退出,我們必須記錄每次已經比較的變量,從而避免進入第二次的比較。Equal函數分配了一組用于比較的結構體,包含每對比較對象的地址(unsafe.Pointer形式保存)和類型。我們要記錄類型的原因是,有些不同的變量可能對應相同的地址。例如,如果x和y都是數組類型,那么x和x[0]將對應相同的地址,y和y[0]也是對應相同的地址,這可以用于區分x與y之間的比較或x[0]與y[0]之間的比較是否進行過了。
~~~
// cycle check
if x.CanAddr() && y.CanAddr() {
xptr := unsafe.Pointer(x.UnsafeAddr())
yptr := unsafe.Pointer(y.UnsafeAddr())
if xptr == yptr {
return true // identical references
}
c := comparison{xptr, yptr, x.Type()}
if seen[c] {
return true // already seen
}
seen[c] = true
}
~~~
這是Equal函數用法的例子:
~~~
fmt.Println(Equal([]int{1, 2, 3}, []int{1, 2, 3})) // "true"
fmt.Println(Equal([]string{"foo"}, []string{"bar"})) // "false"
fmt.Println(Equal([]string(nil), []string{})) // "true"
fmt.Println(Equal(map[string]int(nil), map[string]int{})) // "true"
~~~
Equal函數甚至可以處理類似12.3章中導致Display陷入陷入死循環的帶有環的數據。
~~~
// Circular linked lists a -> b -> a and c -> c.
type link struct {
value string
tail *link
}
a, b, c := &link{value: "a"}, &link{value: "b"}, &link{value: "c"}
a.tail, b.tail, c.tail = b, a, c
fmt.Println(Equal(a, a)) // "true"
fmt.Println(Equal(b, b)) // "true"
fmt.Println(Equal(c, c)) // "true"
fmt.Println(Equal(a, b)) // "false"
fmt.Println(Equal(a, c)) // "false"
~~~
**練習 13.1:** 定義一個深比較函數,對于十億以內的數字比較,忽略類型差異。
**練習 13.2:** 編寫一個函數,報告其參數是否循環數據結構。
### 13.4. 通過cgo調用C代碼
Go程序可能會遇到要訪問C語言的某些硬件驅動函數的場景,或者是從一個C++語言實現的嵌入式數據庫查詢記錄的場景,或者是使用Fortran語言實現的一些線性代數庫的場景。C語言作為一個通用語言,很多庫會選擇提供一個C兼容的API,然后用其他不同的編程語言實現(譯者:Go語言需要也應該擁抱這些巨大的代碼遺產)。
在本節中,我們將構建一個簡易的數據壓縮程序,使用了一個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類型輸入的解壓縮接口。例如:
~~~
package gzip // compress/gzip
func NewWriter(w io.Writer) io.WriteCloser
func NewReader(r io.Reader) (io.ReadCloser, error)
~~~
bzip2壓縮算法,是基于優雅的Burrows-Wheeler變換算法,運行速度比gzip要慢,但是可以提供更高的壓縮比。標準庫的compress/bzip2包目前還沒有提供bzip2壓縮算法的實現。完全從頭開始實現是一個壓縮算法是一件繁瑣的工作,而且 http://bzip.org 已經有現成的libbzip2的開源實現,不僅文檔齊全而且性能又好。
如果是比較小的C語言庫,我們完全可以用純Go語言重新實現一遍。如果我們對性能也沒有特殊要求的話,我們還可以用os/exec包的方法將C編寫的應用程序作為一個子進程運行。只有當你需要使用復雜而且性能更高的底層C接口時,就是使用cgo的場景了(譯注:用os/exec包調用子進程的方法會導致程序運行時依賴那個應用程序)。下面我們將通過一個例子講述cgo的具體用法。
譯注:本章采用的代碼都是最新的。因為之前已經出版的書中包含的代碼只能在Go1.5之前使用。從Go1.6開始,Go語言已經明確規定了哪些Go語言指針可以之間傳入C語言函數。新代碼重點是增加了bz2alloc和bz2free的兩個函數,用于bz_stream對象空間的申請和釋放操作。下面是新代碼中增加的注釋,說明這個問題:
~~~
// 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代碼,對應一個獨立的文件。
*gopl.io/ch13/bzip*
~~~
/* This file is gopl.io/ch13/bzip/bzip2.c, */
/* a simple wrapper for libbzip2 suitable for cgo. */
#include <bzlib.h>
int bz2compress(bz_stream *s, int action,
char *in, unsigned *inlen, char *out, unsigned *outlen) {
s->next_in = in;
s->avail_in = *inlen;
s->next_out = out;
s->avail_out = *outlen;
int r = BZ2_bzCompress(s, action);
*inlen -= s->avail_in;
*outlen -= s->avail_out;
s->next_in = s->next_out = NULL;
return r;
}
~~~
現在讓我們轉到Go語言部分,第一部分如下所示。其中`import "C"`的語句是比較特別的。其實并沒有一個叫C的包,但是這行語句會讓Go編譯程序在編譯之前先運行cgo工具。
~~~
// Package bzip provides a writer that uses bzip2 compression (bzip.org).
package bzip
/*
#cgo CFLAGS: -I/usr/include
#cgo LDFLAGS: -L/usr/lib -lbz2
#include <bzlib.h>
#include <stdlib.h>
bz_stream* bz2alloc() { return calloc(1, sizeof(bz_stream)); }
int bz2compress(bz_stream *s, int action,
char *in, unsigned *inlen, char *out, unsigned *outlen);
void bz2free(bz_stream* s) { free(s); }
*/
import "C"
import (
"io"
"unsafe"
)
type writer struct {
w io.Writer // underlying output stream
stream *C.bz_stream
outbuf [64 * 1024]byte
}
// NewWriter returns a writer for bzip2-compressed streams.
func NewWriter(out io.Writer) io.WriteCloser {
const blockSize = 9
const verbosity = 0
const workFactor = 30
w := &writer{w: out, stream: C.bz2alloc()}
C.BZ2_bzCompressInit(w.stream, blockSize, verbosity, workFactor)
return w
}
~~~
在預處理過程中,cgo工具為生成一個臨時包用于包含所有在Go語言中訪問的C語言的函數或類型。例如C.bz_stream和C.BZ2_bzCompressInit。cgo工具通過以某種特殊的方式調用本地的C編譯器來發現在Go源文件導入聲明前的注釋中包含的C頭文件中的內容(譯注:`import "C"`語句前僅挨著的注釋是對應cgo的特殊語法,對應必要的構建參數選項和C語言代碼)。
在cgo注釋中還可以包含#cgo指令,用于給C語言工具鏈指定特殊的參數。例如CFLAGS和LDFLAGS分別對應傳給C語言編譯器的編譯參數和鏈接器參數,使它們可以特定目錄找到bzlib.h頭文件和libbz2.a庫文件。這個例子假設你已經在/usr目錄成功安裝了bzip2庫。如果bzip2庫是安裝在不同的位置,你需要更新這些參數(譯注:這里有一個從純C代碼生成的cgo綁定,不依賴bzip2靜態庫和操作系統的具體環境,具體請訪問 https://github.com/chai2010/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類型并不相同,即使它們具有相同的大小也是不同的類型。
~~~
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方法有著類似的結構,通過一個循環將剩余的壓縮數據刷新到輸出緩存。
~~~
// Close flushes the compressed data and closes the stream.
// It does not close the underlying io.Writer.
func (w *writer) Close() error {
if w.stream == nil {
panic("closed")
}
defer func() {
C.BZ2_bzCompressEnd(w.stream)
C.bz2free(w.stream)
w.stream = nil
}()
for {
inlen, outlen := C.uint(0), C.uint(cap(w.outbuf))
r := C.bz2compress(w.stream, C.BZ_FINISH, nil, &inlen,
(*C.char)(unsafe.Pointer(&w.outbuf)), &outlen)
if _, err := w.w.Write(w.outbuf[:outlen]); err != nil {
return err
}
if r == C.BZ_STREAM_END {
return nil
}
}
}
~~~
壓縮完成后,Close方法用了defer函數確保函數退出前調用C.BZ2_bzCompressEnd和C.bz2free釋放相關的C語言運行時資源。此刻w.stream指針將不再有效,我們將它設置為nil以保證安全,然后在每個方法中增加了nil檢測,以防止用戶在關閉后依然錯誤使用相關方法。
上面的實現中,不僅僅寫是非并發安全的,甚至并發調用Close和Write方法也可能導致程序的的崩潰。修復這個問題是練習13.3的內容。
下面的bzipper程序,使用我們自己包實現的bzip2壓縮命令。它的行為和許多Unix系統的bzip2命令類似。
*gopl.io/ch13/bzipper*
~~~
// Bzipper reads input, bzip2-compresses it, and writes it out.
package main
import (
"io"
"log"
"os"
"gopl.io/ch13/bzip"
)
func main() {
w := bzip.NewWriter(os.Stdout)
if _, err := io.Copy(w, os.Stdin); err != nil {
log.Fatalf("bzipper: %v\n", err)
}
if err := w.Close(); err != nil {
log.Fatalf("bzipper: close: %v\n", err)
}
}
~~~
在上面的場景中,我們使用bzipper壓縮了/usr/share/dict/words系統自帶的詞典,從938,848字節壓縮到335,405字節。大約是原始數據大小的三分之一。然后使用系統自帶的bunzip2命令進行解壓。壓縮前后文件的SHA256哈希碼是相同了,這也說明了我們的壓縮工具是正確的。(如果你的系統沒有sha256sum命令,那么請先按照練習4.2實現一個類似的工具)
~~~
$ go build gopl.io/ch13/bzipper
$ wc -c < /usr/share/dict/words
938848
$ sha256sum < /usr/share/dict/words
126a4ef38493313edc50b86f90dfdaf7c59ec6c948451eac228f2f3a8ab1a6ed -
$ ./bzipper < /usr/share/dict/words | wc -c
335405
$ ./bzipper < /usr/share/dict/words | bunzip2 | sha256sum
126a4ef38493313edc50b86f90dfdaf7c59ec6c948451eac228f2f3a8ab1a6ed -
~~~
我們演示了如何將一個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.4:** 因為C庫依賴的限制。 使用os/exec包啟動/bin/bzip2命令作為一個子進程,提供一個純Go的bzip.NewWriter的替代實現(譯注:雖然是純Go實現,但是運行時將依賴/bin/bzip2命令,其他操作系統可能無法運行)。
### 13.5. 幾點忠告
我們在前一章結尾的時候,我們警告要謹慎使用reflect包。那些警告同樣適用于本章的unsafe包。
高級語言使得程序員不用在關心真正運行程序的指令細節,同時也不再需要關注許多如內存布局之類的實現細節。因為高級語言這個絕緣的抽象層,我們可以編寫安全健壯的,并且可以運行在不同操作系統上的具有高度可移植性的程序。
但是unsafe包,它讓程序員可以透過這個絕緣的抽象層直接使用一些必要的功能,雖然可能是為了獲得更好的性能。但是代價就是犧牲了可移植性和程序安全,因此使用unsafe包是一個危險的行為。我們對何時以及如何使用unsafe包的建議和我們在11.5節提到的Knuth對過早優化的建議類似。大多數Go程序員可能永遠不會需要直接使用unsafe包。當然,也永遠都會有一些需要使用unsafe包實現會更簡單的場景。如果確實認為使用unsafe包是最理想的方式,那么應該盡可能將它限制在較小的范圍,那樣其它代碼就忽略unsafe的影響。
現在,趕緊將最后兩章拋入腦后吧。編寫一些實實在在的應用是真理。請遠離reflect的unsafe包,除非你確實需要它們。
最后,用Go快樂地編程。我們希望你能像我們一樣喜歡Go語言。
- Go語言程序設計
- Go語言圣經(中文版)
- 譯者序
- 前言
- 第一章 入門
- 第二章 程序結構
- 第三章 基礎數據類型
- 第四章 復合數據類型
- 第五章 函數
- 第六章 方法
- 第七章 接口
- 7.4. flag.Value接口
- 7.6. sort.Interface接口
- 7.7. http.Handler接口
- 第八章 Goroutines和Channels
- 第九章 基于共享變量的并發
- 9.2. sync.Mutex互斥鎖
- 第十章 包和工具
- 第十一章 測試
- 第十二章 反射
- 12.2. reflect.Type和reflect.Value
- 12.5. 通過reflect.Value修改值
- 第13章 底層編程
- 13.1. unsafe.Sizeof, Alignof 和 Offsetof
- 附錄