在cgo的官方文檔中有一小節特意介紹了cgo中傳遞c語言和go語言指針之間的傳遞,因爲裏面講得比較抽象而且缺乏例子,所以經過這篇文章總結cgo指針傳遞的注意事項。函數
在官方文檔和本篇總結中,Go指針指的是指向Go分配的內存的指針(例如使用&
運算符或者調用new
函數獲取的指針)。而C指針指的是C分配的內存的指針(例如調用malloc
函數獲取的指針)。一個指針是Go指針仍是C指針,是根據內存如何分配判斷的,與指針的類型無關。ui
Go調用C Code時,Go傳遞給C Code的Go指針所指的Go Memory中不能包含任何指向Go Memory的Pointer。
值得注意的是,Go是能夠傳遞給C Code的Go指針的,可是這個指針裏面不能包含任何指向Go Memory的Pointer。指針
package main /* #include <stdio.h> struct Foo { int a; int *p; }; void plusOne(struct Foo *f) { (f->a)++; *(f->p)++; } */ import "C" import "unsafe" import "fmt" func main() { f := &C.struct_Foo{} f.a = 5 f.p = (*C.int)((unsafe.Pointer)(new(int))) // f.p = &f.a C.plusOne(f) fmt.Println(int(f.a)) }
在以上代碼能夠看出,Go Code向C Code傳遞了一個指向Go Memory(Go分配的)指針f,但f指向的Go Memory中有一個指針p指向了另外一處Go Memory:new(int)
。當使用go build
編譯這個文件時,是能夠經過編譯的,而後在運行時會發生以下報錯:panic runtime error: cgo argument has Go pointer to Go pointer
。code
Go調用C Code時,若是傳遞的是一個指向struct field的指針,那麼「Go Memory」專指這個field所佔用的內存,即使struct中有其餘field指向其餘Go Memory也不要緊。
將上面例子改成只傳入指向struct field的指針。以下:內存
package main /* #include <stdio.h> struct Foo { int a; int *p; }; void plusOne(int *i) { (*i)++; } */ import "C" import ( "fmt" "unsafe" ) func main() { f := &C.struct_Foo{} f.a = 5 f.p = (*C.int)((unsafe.Pointer)(new(int)) C.plusOne(&f.a) fmt.Println(int(f.a)) }
直接指向go run
,打印結果爲6
。能夠看出,由於此次調用只傳遞單個field指針,指向這個field所佔用的內存,而這個field也沒有嵌套其餘指向Go Memory的指針,所以這是符合規範的調用,不會觸發panic。element
和傳遞struct field不一樣,傳遞一個指向slice或者array中的element指針時,須要考慮的Go Memory的範圍不單單是這個element,而是整個array或這個slice背後的underlying array所佔用的內存區域,要保證整個區域內不包含任何指向Go Memory的指針。文檔
package main /* #include <stdio.h> void plusOne(int **i) { (**i)++; } */ import "C" import ( "fmt" "unsafe" ) func main() { s1 := make([]*int, 5) var a int = 5 s1[1] = &a C.plusOne((**C.int)((unsafe.Pointer)(&s1[0]))) fmt.Println(s1[0]) }
從以上代碼能夠看出,傳遞給C的是slice第一個element的地址,並不包括指向Go Memory的指針,但因爲第二個element保存了另一塊Go Memory的地址(&a),當運行go run
時,得到報錯:panic runtime error: cgo argument has Go pointer to Go pointer
。io
C調用的Go函數不能返回指向Go分配的內存的指針。
package main // extern int* goAdd(int, int); // // int cAdd(int a, int b) { // int *i = goAdd(a, b); // return *i; // } import "C" import "fmt" // export goAdd func goAdd(a, b C.int) { c := a + b return &c } func main() { var a, b int = 5, 6 i := C.cAdd(C.int(a), C.int(b)) fmt.Println(int(i)) }
上面代碼中,goAdd這個Go函數返回了一個指向Go分配的內存(&c)的指針。運行上述代碼,結果以下:panic runtime error: cgo result has Go pointer
。編譯
Go Code不能在C分配的內存中存儲指向Go分配的內存的指針。
package main // #include <stdlib.h> // extern void goFoo(int**); // // void cFoo() { // int **p = malloc(sizeof(int*)); // goFoo(p); // } import "C" //export goFoo func goFoo(p **C.int) { *p = new(C.int) } func main() { C.cFoo() }
針對此例,默認的GODEBUG=cgocheck=1是正常運行的,將GODEBUG=cgocheck=2則會發生報錯:fatal error: Go pointer stored into non-Go memory
。效率
以上規則會在運行時動態檢測,能夠經過設置GODEBUG環境變量修改檢測程度,默認值是GODEBUG=cgocheck=1,能夠經過設置爲0取消這些檢測,也能夠經過設置爲2來提升檢測標準,但這會犧牲運行的效率。
此外,也能夠經過使用unsafe
包來逃脫這些限制,並且C語言方面也無法使用什麼特殊的機制來限制調用Go。儘管如此,若是程序打破了上面的限制,極可能會以一種沒法預料的方式調用失敗。
cgo中,Go與C的內存應該保持着相對獨立,指針之間的傳遞應該儘可能避免嵌套不一樣內存的指針(如C中保存Go指針)。指針之間傳遞的規則不是絕對要遵照的,能夠經過多種方式忽視檢測,可是這每每致使沒法預料的結果。