<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Go指針的使用限制和突破之路

          共 6761字,需瀏覽 14分鐘

           ·

          2021-02-05 09:33

          這篇文章跟大家聊一下 Go 語言指針這個話題,相較于 C 而言,Go 語言在設(shè)計(jì)時(shí)為了使用安全給指針在類型和運(yùn)算上增加了限制,這讓Go程序員既可以享受指針帶來的便利,又避免了指針的危險(xiǎn)性。除了常規(guī)的指針外,Go 語言在 unsafe 包里其實(shí)還通過 unsafe.Pointer 提供了通用指針,通過這個通用指針以及 unsafe 包的其他幾個功能又讓使用者能夠繞過 Go 語言的類型系統(tǒng)直接操作內(nèi)存進(jìn)行例如:指針類型轉(zhuǎn)換,讀寫結(jié)構(gòu)體私有成員這樣操作。網(wǎng)管覺得正是因?yàn)楣δ軓?qiáng)大同時(shí)伴隨著操作不慎讀寫了錯誤的內(nèi)存地址即會造成的嚴(yán)重后果所以 Go 語言的設(shè)計(jì)者才會把這些功能放在 unsafe 包里。其實(shí)也沒有想得那么不安全,掌握好了使用得當(dāng)還是能帶來很大的便利的,在一些偏向底層的源碼中 unsafe 包使用的頻率還是不低的。對于勵志成為高階 Gopher 的各位,這也是一項(xiàng)必不可少需要掌握的技能啦。接下來網(wǎng)管就帶大家從基本的指針使用方法和限制開始看看怎么用 unsafe 包跨過這些限制直接讀寫內(nèi)存。

          基礎(chǔ)知識

          指針保存著一個值的內(nèi)存地址,類型 *T代表指向T 類型值的指針。其零值為nil

          &操作符為它的操作數(shù)生成一個指針。

          i?:=?42
          p?=?&i

          *操作符則會取出指針指向地址的值,這個操作也叫做“解引用”。

          fmt.Println(*p)?//?通過指針p讀取存儲的值
          *p?=?21?????????//?通過指針p設(shè)置p執(zhí)行的內(nèi)存地址存儲的值

          為什么需要指針類型呢?參考一個從go101網(wǎng)站上看到的例子 :

          package?main

          import?"fmt"

          func?double(x?int)?{?
          ???x?+=?x
          }

          func?main()?{?
          ???var?a?=?3?
          ???double(a)
          ????fmt.Println(a)?//?3
          }

          在 double 函數(shù)里將 a 翻倍,但是例子中的函數(shù)卻做不到。因?yàn)?Go 語言的函數(shù)傳參都是值傳遞。double 函數(shù)里的 x 只是實(shí)參 a 的一個拷貝,在函數(shù)內(nèi)部對 x 的操作不能反饋到實(shí)參 a。

          把參數(shù)換成一個指針就可以解決這個問題了。

          package?main
          import?"fmt"

          func?double(x?*int)?{?
          ???*x?+=?*x
          ????x?=?nil
          }

          func?main()?{
          ????var?a?=?3
          ????double(&a)
          ????fmt.Println(a)?//?6
          ????p?:=?&a
          ????double(p)
          ????fmt.Println(a,?p?==?nil)?//?12?false
          }

          上面的程序乍一看你可能對下面這一行代碼有些疑惑

          x?=?nil

          稍微思考一下上面說的Go語言里面參數(shù)都是值傳遞,你就會知道這一行代碼根本不影響外面的變量 a。因?yàn)閰?shù)都是值傳遞,所以函數(shù)內(nèi)的 x 也只是對 &a 的一個拷貝。

          *x?+=?*x

          這一句把 x 指向的值(也就是 &a 指向的值,即變量 a)變?yōu)樵瓉淼?2 倍。但是對 x 本身(一個指針)的操作卻不會影響外層的 a,所以在double函數(shù)內(nèi)部的 x=nil 不會影響外面。

          指針的限制

          相較于 C 語言指針的靈活,Go 語言里指針多了不少限制,不過這讓我們:既可以享受指針帶來的便利,又避免了指針的危險(xiǎn)性。下面就簡單說一下 Go 對指針操作的一些限制

          限制一:指針不能參與運(yùn)算

          來看一個簡單的例子:

          package?main

          import?"fmt"

          func?main()?{
          ?a?:=?5
          ?p?:=?a
          ?fmt.Println(p)
          ?p?=?&a?+?3
          }

          上面的代碼將不能通過編譯,會報(bào)編譯錯誤:

          invalid?operation:?&a?+?3?(mismatched?types?*int?and?int)

          也就是說 Go 不允許對指針進(jìn)行數(shù)學(xué)運(yùn)算。

          限制二:不同類型的指針不允許相互轉(zhuǎn)換。

          下面的程序同樣也不能編譯成功:

          package?main

          func?main()?{
          ?var?a?int?=?100
          ?var?f?*float64
          ?f?=?*float64(&a)
          }

          限制三:不同類型的指針不能比較和相互賦值

          這條限制同上面的限制二,因?yàn)橹羔樦g不能做類型轉(zhuǎn)換,所以也沒法使用==或者!=進(jìn)行比較了,同樣不同類型的指針變量相互之間不能賦值。比如下面這樣,也是會報(bào)編譯錯誤。

          package?main

          func?main()?{
          ?var?a?int?=?100
          ?var?f?*float64
          ?f?=?&a
          }

          Go語言的指針是類型安全的,但它有很多限制,所以 Go 還有提供了可以進(jìn)行類型轉(zhuǎn)換的通用指針,這就是 unsafe 包提供的 unsafe.Pointer。在某些情況下,它會使代碼更高效,當(dāng)然如果掌握不好就使用,也更容易讓程序崩潰。

          unsafe 包

          unsafe 包用于編譯階段可以繞過 Go 語言的類型系統(tǒng),直接操作內(nèi)存。例如,利用 unsafe 包操作一個結(jié)構(gòu)體的未導(dǎo)出成員。unsafe 包讓我可以直接讀寫內(nèi)存的能力。

          unsafe包只有兩個類型,三個函數(shù),但是功能很強(qiáng)大。

          type?ArbitraryType?int
          type?Pointer?*ArbitraryType

          func?Sizeof(x?ArbitraryType)?uintptr
          func?Offsetof(x?ArbitraryType)?uintptr
          func?Alignof(x?ArbitraryType)?uintptr

          ArbitraryTypeint的一個別名,在 Go 中ArbitraryType有特殊的意義。代表一個任意Go表達(dá)式類型。Pointerint指針類型的一個別名,在 Go 中可以把任意指針類型轉(zhuǎn)換成unsafe.Pointer類型。

          三個函數(shù)的參數(shù)均是ArbitraryType類型,就是接受任何類型的變量。

          • Sizeof接受任意類型的值(表達(dá)式),返回其占用的字節(jié)數(shù),這和c語言里面不同,c語言里面sizeof函數(shù)的參數(shù)是類型,而這里是一個值,比如一個變量。
          • Offsetof:返回結(jié)構(gòu)體成員在內(nèi)存中的位置距離結(jié)構(gòu)體起始處的字節(jié)數(shù),所傳參數(shù)必須是結(jié)構(gòu)體的成員(結(jié)構(gòu)體指針指向的地址就是結(jié)構(gòu)體起始處的地址,即第一個成員的內(nèi)存地址)。
          • Alignof返回變量對齊字節(jié)數(shù)量,這個函數(shù)雖然接收的是任何類型的變量,但是有一個前提,就是變量要是一個struct類型,且還不能直接將這個struct類型的變量當(dāng)作參數(shù),只能將這個struct類型變量的值當(dāng)作參數(shù),具體細(xì)節(jié)咱們到以后聊內(nèi)存對齊的文章里再說。

          注意以上三個函數(shù)返回的結(jié)果都是 uintptr 類型,這和 unsafe.Pointer 可以相互轉(zhuǎn)換。三個函數(shù)都是在編譯期間執(zhí)行

          unsafe.Pointer

          unsafe.Pointer稱為通用指針,官方文檔對該類型有四個重要描述:

          1. 任何類型的指針都可以被轉(zhuǎn)化為Pointer
          2. Pointer可以被轉(zhuǎn)化為任何類型的指針
          3. uintptr可以被轉(zhuǎn)化為Pointer
          4. Pointer可以被轉(zhuǎn)化為uintptr

          unsafe.Pointer是特別定義的一種指針類型(譯注:類似C語言中的void類型的指針),在Go 語言中是用于各種指針相互轉(zhuǎn)換的橋梁,它可以持有任意類型變量的地址

          什么叫"可以持有任意類型變量的地址"呢?意思就是使用 unsafe.Pointer 轉(zhuǎn)換的變量,該變量一定要是指針類型,否則編譯會報(bào)錯。

          a?:=?1
          b?:=?unsafe.Pointer(a)?//報(bào)錯
          b?:=?unsafe.Pointer(&a)?//?正確

          和普通指針一樣,unsafe.Pointer 指針也是可以比較的,并且支持和 nil 比較判斷是否為空指針。

          unsafe.Pointer 不能直接進(jìn)行數(shù)學(xué)運(yùn)算,但可以把它轉(zhuǎn)換成 uintptr,對 uintptr 類型進(jìn)行數(shù)學(xué)運(yùn)算,再轉(zhuǎn)換成 unsafe.Pointer 類型

          //?uintptr、unsafe.Pointer和普通指針之間的轉(zhuǎn)換關(guān)系
          uintptr?<==>?unsafe.Pointer?<==>?*T

          uintptr

          uintptr是 Go 語言的內(nèi)置類型,是能存儲指針的整型,在64位平臺上底層的數(shù)據(jù)類型是 uint64。

          //?uintptr?is?an?integer?type?that?is?large?enough?to?hold?the?bit?pattern?of
          //?any?pointer.
          type?uintptr?uintptr

          typedef?unsigned?long?long?int??uint64;
          typedef?uint64??????????uintptr;

          一個unsafe.Pointer指針也可以被轉(zhuǎn)化為uintptr類型,然后保存到uintptr類型的變量中(注:這個變量只是和當(dāng)前指針有相同的一個數(shù)字值,并不是一個指針),然后用以做必要的指針數(shù)值運(yùn)算。(uintptr是一個無符號的整型數(shù),足以保存一個地址)這種轉(zhuǎn)換雖然也是可逆的,但是隨便將一個 uintptr 轉(zhuǎn)為 unsafe.Pointer指針可能會破壞類型系統(tǒng),因?yàn)椴⒉皇撬械臄?shù)字都是有效的內(nèi)存地址。

          還有一點(diǎn)要注意的是,uintptr 并沒有指針的語義,意思就是存儲 uintptr 值的內(nèi)存地址在Go發(fā)生GC時(shí)會被回收。而 unsafe.Pointer 有指針語義,可以保護(hù)它不會被垃圾回收。

          聊了這么多概念性的話題,接下來網(wǎng)管帶大家一起看看怎么使用 unsafe.Pointer 進(jìn)行指針轉(zhuǎn)換以及結(jié)合 uintptr 讀寫結(jié)構(gòu)體的私有成員。

          應(yīng)用示例

          使用unsafe.Pointer進(jìn)行指針類型轉(zhuǎn)換

          import?(
          ????"fmt"
          ????"reflect"
          ????"unsafe"
          )
          ?
          func?main()?{
          ?
          ????v1?:=?uint(12)
          ????v2?:=?int(13)
          ?
          ????fmt.Println(reflect.TypeOf(v1))?//uint
          ????fmt.Println(reflect.TypeOf(v2))?//int
          ?
          ????fmt.Println(reflect.TypeOf(&v1))?//*uint
          ????fmt.Println(reflect.TypeOf(&v2))?//*int
          ?
          ????p?:=?&v1
          ????p?=?(*uint)(unsafe.Pointer(&v2))?//使用unsafe.Pointer進(jìn)行類型的轉(zhuǎn)換
          ?
          ????fmt.Println(reflect.TypeOf(p))?//?*unit
          ????fmt.Println(*p)?//13
          }

          使用unsafe.Pointer 讀寫結(jié)構(gòu)體的私有成員

          通過 Offsetof 方法可以獲取結(jié)構(gòu)體成員的偏移量,進(jìn)而獲取成員的地址,讀寫該地址的內(nèi)存,就可以達(dá)到改變成員值的目的。

          這里有一個內(nèi)存分配相關(guān)的事實(shí):結(jié)構(gòu)體會被分配一塊連續(xù)的內(nèi)存,結(jié)構(gòu)體的地址也代表了第一個成員的地址。

          package?main
          ?
          import?(
          ????"fmt"
          ????"unsafe"
          )
          ?
          func?main()?{
          ?
          ????var?x?struct?{
          ????????a?int
          ????????b?int
          ????????c?[]int
          ????}
          ?
          ????// unsafe.Offsetof 函數(shù)的參數(shù)必須是一個字段, ?比如 x.b, ?方法會返回 b 字段相對于 x 起始地址的偏移量, 包括可能的空洞。

          ????//?指針運(yùn)算 uintptr(unsafe.Pointer(&x))?+ unsafe.Offsetof(x.b)。
          ????
          ????//?和?pb?:=?&x.b?等價(jià)
          ????pb?:=?(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&x))?+?unsafe.Offsetof(x.b)))

          ????*pb?=?42
          ????fmt.Println(x.b)?//?"42"
          }

          上面的寫法盡管很繁瑣,但在這里并不是一件壞事,因?yàn)檫@些功能應(yīng)該很謹(jǐn)慎地使用。不要試圖引入一個uintptr類型的臨時(shí)變量,因?yàn)樗赡軙茐拇a的安全性

          如果改為下面這種用法是有風(fēng)險(xiǎn)的:

          tmp?:=?uintptr(unsafe.Pointer(&x))?+?unsafe.Offsetof(x.b)
          pb?:=?(*int16)(unsafe.Pointer(tmp))
          *pb?=?42

          隨著程序執(zhí)行的進(jìn)行,goroutine 會經(jīng)常發(fā)生棧擴(kuò)容或者棧縮容,會把舊棧內(nèi)存的數(shù)據(jù)拷貝到新棧區(qū)然后更改所有指針的指向。一個 unsafe.Pointer 是一個指針,因此當(dāng)它指向的數(shù)據(jù)被移動到新棧區(qū)后指針也會被更新。但是uintptr 類型的臨時(shí)變量只是一個普通的數(shù)字,所以其值不會該被改變。上面錯誤的代碼因?yàn)橐胍粋€非指針的臨時(shí)變量 tmp,導(dǎo)致系統(tǒng)無法正確識別這個是一個指向變量 x 的指針。當(dāng)?shù)诙€語句執(zhí)行時(shí),變量 x 的數(shù)據(jù)可能已經(jīng)被轉(zhuǎn)移,這時(shí)候臨時(shí)變量tmp也就不再是現(xiàn)在的 &x.b 的地址。第三個語句向之前無效地址空間的賦值語句將讓整個程序崩潰。

          string 和 []byte 零拷貝轉(zhuǎn)換

          這是一個非常經(jīng)典的例子。實(shí)現(xiàn)字符串和 bytes 切片之間的零拷貝轉(zhuǎn)換。

          string和[]byte 在運(yùn)行時(shí)的類型表示為reflect.StringHeaderreflect.SliceHeader

          type?SliceHeader?struct?{
          ?Data?uintptr
          ?Len??int
          ?Cap??int
          }

          type?StringHeader?struct?{
          ?Data?uintptr
          ?Len??int
          }

          只需要共享底層 []byte 數(shù)組就可以實(shí)現(xiàn)零拷貝轉(zhuǎn)換。

          代碼比較簡單,不作詳細(xì)解釋。通過構(gòu)造reflect.StringHeaderreflect.SliceHeader,來完成 string []byte 之間的轉(zhuǎn)換。

          import?(
          ?"fmt"
          ?"reflect"
          ?"unsafe"
          )

          func?main()?{
          ?s?:=?"Hello?World"
          ?b?:=?string2bytes(s)
          ?fmt.Println(b)
          ?s?=?bytes2string(b)
          ?fmt.Println(s)

          }

          func?string2bytes(s?string)?[]byte?{
          ?stringHeader?:=?(*reflect.StringHeader)(unsafe.Pointer(&s))

          ?bh?:=?reflect.SliceHeader{
          ??Data:?stringHeader.Data,
          ??Len:?stringHeader.Len,
          ??Cap:?stringHeader.Len,
          ?}

          ?return?*(*[]byte)(unsafe.Pointer(&bh))
          }

          func?bytes2string(b?[]byte)?string?{
          ?sliceHeader?:=?(*reflect.SliceHeader)(unsafe.Pointer(&b))

          ?sh?:=?reflect.StringHeader{
          ??Data:?sliceHeader.Data,
          ??Len:??sliceHeader.Len,
          ?}

          ?return?*(*string)(unsafe.Pointer(&sh))
          }

          總結(jié)

          Go 的源碼中也在大量使用 unsafe 包,通過 unsafe 包繞過 Go 指針的限制,達(dá)到直接操作內(nèi)存的目的,使用它有一定的風(fēng)險(xiǎn)性,但是在一些場景下,可以提升代碼的效率。

          參考資料



          推薦閱讀


          福利

          我為大家整理了一份從入門到進(jìn)階的Go學(xué)習(xí)資料禮包,包含學(xué)習(xí)建議:入門看什么,進(jìn)階看什么。關(guān)注公眾號 「polarisxu」,回復(fù)?ebook?獲取;還可以回復(fù)「進(jìn)群」,和數(shù)萬 Gopher 交流學(xué)習(xí)。

          瀏覽 60
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  日韩午夜久久 | 成人网址大全 | 日韩v欧美v | 91美女裸体网站 | 天天干人妻|