掌握 cgo 的字符串函數
cgo[1] 的大量文檔都提到過,它提供了四個用於轉換 Go 和 C 類型的字符串的函數,都是通過複製數據來實現。在 CGo 的文檔中有簡潔的解釋,但我認爲解釋得太簡潔了,因爲文檔只涉及了定義中的某些特定字符串,而忽略了兩個很重要的注意事項。我曾經踩過這裏的坑,現在我要詳細解釋一下。
四個函數分別是:
func C.CString(string) *C.char
func C.GoString(*C.char) string
func C.GoStringN(*C.char, C.int) string
func C.GoBytes(unsafe.Pointer, C.int) []byte
C.CString()
等價於 C 的 strdup()
,像文檔中提到的那樣,把 Go 的字符串複製爲可以傳遞給 C 函數的 C 的 char *
。很討厭的一件事是,由於 Go 和 CGo 類型的定義方式,調用 C.free
時需要做一個轉換:
cs := C.CString("a string")
C.free(unsafe.Pointer(cs))
請留意,Go 字符串中可能嵌入了 \0
字符,而 C 字符串不會。如果你的 Go 字符串中有 \0
字符,當你調用 C.CString()
時,C 代碼會從 \0
字符處截斷你的字符串。這往往不會被注意到,但有時文本並不保證不含 null 字符 [2]。
C.GoString()
也等價於 strdup()
,但與 C.CString()
相反,是把 C 字符串轉換爲 Go 字符串。你可以用它定義結構體的字段,或者是聲明爲 C 的 char *
(在 Go 中叫 *C.cahr
) 的其他變量,抑或其他的一些變量(我們後面會看到)。
C.GoStringN()
等價於 C 的 memmove()
,與 C 中普通的字符串函數不同。**它把整個 N 長度的 C buffer 複製爲一個 Go 字符串,不單獨處理 null 字符。**再詳細點,它也通過複製來實現。如果你有一個定義爲 char feild[64]
的結構體的字段,然後調用了 C.GoStringN(&field, 64)
,那麼你得到的 Go 字符串一定是 64 個字符,字符串的末尾有可能是一串 \0
字符。
(我認爲這是 cgo 文檔中的一個 bug。它宣稱 GoStringN 的入參是一個 C 的字符串,但實際上很明顯不是,因爲 C 的字符串不能以 null 字符結束,而 GoStringN 不會在 null 字符處結束處理。)
C.GoBytes()
是 C.GoStringN()
的另一個版本,不返回 string
而是返回 []byte
。它沒有宣稱以 C 字符串作爲入參,它僅僅是對整個 buffer 做了內存拷貝。
如果你要拷貝的東西不是以 null 字符結尾的 C 字符串,而是固定長度的 memory buffer,那麼 C.GoString()
正好能滿足需求;它避開了 C 中傳統的問題處理不是 C 字符串的 ’string‘[3]。然而,如果你要處理定義爲 char field[N]
的結構體字段這種限定長度的 C 字符串時,這些函數_都不能_滿足需求。
傳統語義的結構體中固定長度的字符串變量,定義爲 char field[N]
的字段,以及 “包含一個字符串” 等描述,都表示當且僅當字符串有足夠空間時以 null 字符結尾,換句話說,字符串最多有 N-1 個字符。如果字符串正好有 N 個字符,那麼它不會以 null 字符結尾。這是 C 代碼中諸多 bug 的根源 [4],也不是一個好的 API,但我們卻擺脫不了這個 API。每次我們遇到這樣的字段,文檔不會明確告訴你字段的內容並不一定是 null 字符結尾的,你需要自己假設你有這種 API。
C.GoString()
或 C.GoStringN()
都不能正確處理這些字段。使用 GoStringN()
相對來說出錯更少;它僅僅返回一個末尾有一串 \0
字符長度爲 N 的 Go 字符串(如果你僅僅是把這些字段打印出來,那麼你可能不會留意到;我經常幹這種事)。使用有誘惑力的 GoString()
更是引狼入室,因爲它內部會對入參做 strlen()
;如果字符末尾沒有 null 字符,strlen()
會訪問越界的內存地址。如果你走運,你得到的 Go 字符串末尾會有大量的垃圾。如果你不走運,你的 Go 程序出現段錯誤,因爲 strlen()
訪問了未映射的內存地址。
(總的來說,如果字符串末尾出現了大量垃圾,通常意味着在某處有不含結束符的 C 字符串。)
你需要的是與 C 的 strndup()
等價的 Go 函數,以此來確保複製不超過 N 個字符且在 null 字符處終止。下面是我寫的版本,不保證無錯誤:
func strndup(cs *C.char, len int) string {
s := C.GoStringN(cs, C.int(len))
i := strings.IndexByte(s, 0)
if i == -1 {
return s
}
return C.GoString(cs)
}
由於有 Go 的字符串怎樣佔用內存 [5] 的問題,這段代碼做了些額外的工作來最小化額外的內存佔用。你可能想用另一種方法,返回一個 GoStringN()
字符串的切片。你也可以寫複雜的代碼,根據 i 和 len 的不同來決定選用哪種方法。
更新:Ian Lance Taylor 給我展示了份更好的代碼 [6]:
func strndup(cs *C.char, len int) string {
return C.GoStringN(cs, C.int(C.strnlen(cs, C.size_t(len))))
}
是的,這裏有大量的轉換。這篇文章就是你看到的 Go 和 Gco 類型的結合。
via: https://utcc.utoronto.ca/~cks/space/blog/programming/GoCGoStringFunctions
作者:ChrisSiebenmann[7] 譯者:lxbwolf[8] 校對:polaris1119[9]
本文由 GCTT[10] 原創編譯,Go 中文網 [11] 榮譽推出
參考資料
[1]
cgo: https://github.com/golang/go/wiki/cgo
[2]
有時文本並不保證不含 null 字符: https://utcc.utoronto.ca/~cks/space/blog/programming/BeSureItsACString
[3]
處理不是 C 字符串的 ’string‘: https://utcc.utoronto.ca/~cks/space/blog/programming/BeSureItsACString
[4]
N]` 的字段,以及 “包含一個字符串” 等描述,都表示當且僅當字符串有足夠空間時以 null 字符結尾,換句話說,字符串最多有 N-1 個字符。如果字符串正好有 N 個字符,那麼它不會以 null 字符結尾。這是 [C 代碼中諸多 bug 的根源: https://utcc.utoronto.ca/~cks/space/blog/programming/UnixAPIMistake
[5]
Go 的字符串怎樣佔用內存: https://utcc.utoronto.ca/~cks/space/blog/programming/GoStringsMemoryHolding
[6]
Ian Lance Taylor 給我展示了份更好的代碼: https://github.com/golang/go/issues/12428#issuecomment-136581154
[7]
ChrisSiebenmann: https://utcc.utoronto.ca/~cks/space/People/ChrisSiebenmann
[8]
lxbwolf: https://github.com/lxbwolf
[9]
polaris1119: https://github.com/polaris1119
[10]
GCTT: https://github.com/studygolang/GCTT
[11]
Go 中文網: https://studygolang.com/
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/RFMKhSmc22GwxeIeI8MAGQ