<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指針的扒得干干凈凈

          共 9210字,需瀏覽 19分鐘

           ·

          2021-08-30 05:12

          今天想在這篇文章里好好跟大家聊一下 Go 語言指針這個話題,相較于 C 而言,Go 語言在設(shè)計時為了使用安全給指針在類型和運算上增加了限制,這讓Go程序員既可以享受指針帶來的便利,又避免了指針的危險性。

          除了常規(guī)的指針外,Go 語言在 unsafe 包里其實還通過 unsafe.Pointer 提供了通用指針,通過這個通用指針以及 unsafe 包的其他幾個功能又讓使用者能夠繞過 Go 語言的類型系統(tǒng)直接操作內(nèi)存進(jìn)行例如:指針類型轉(zhuǎn)換,讀寫結(jié)構(gòu)體私有成員這樣操作。

          網(wǎng)管覺得正是因為功能強(qiáng)大同時伴隨著操作不慎讀寫了錯誤的內(nèi)存地址即會造成的嚴(yán)重后果所以 Go 語言的設(shè)計者才會把這些功能放在 unsafe 包里。其實也沒有想得那么不安全,掌握好了使用得當(dāng)還是能帶來很大的便利的,在一些偏向底層的源碼中 unsafe 包使用的頻率還是不低的。

          對于勵志成為高階 Gopher 的各位,這也是一項必不可少需要掌握的技能啦。接下來網(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ù)卻做不到。因為 Go 語言的函數(shù)傳參都是值傳遞。double 函數(shù)里的 x 只是實參 a 的一個拷貝,在函數(shù)內(nèi)部對 x 的操作不能反饋到實參 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。因為參數(shù)都是值傳遞,所以函數(shù)內(nèi)的 x 也只是對 &a 的一個拷貝。

          *x += *x

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

          # 指針的限制

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

           限制一:指針不能參與運算

          來看一個簡單的例子:

          package main

          import "fmt"

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

          上面的代碼將不能通過編譯,會報編譯錯誤:

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

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

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

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

          package main

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

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

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

          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)換的變量,該變量一定要是指針類型,否則編譯會報錯。

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

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

          unsafe.Pointer 不能直接進(jìn)行數(shù)學(xué)運算,但可以把它轉(zhuǎn)換成 uintptr,對 uintptr 類型進(jìn)行數(shù)學(xué)運算,再轉(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ù)值運算。(uintptr是一個無符號的整型數(shù),足以保存一個地址)這種轉(zhuǎn)換雖然也是可逆的,但是隨便將一個 uintptr 轉(zhuǎn)為 unsafe.Pointer指針可能會破壞類型系統(tǒng),因為并不是所有的數(shù)字都是有效的內(nèi)存地址。

          還有一點要注意的是,uintptr 并沒有指針的語義,意思就是存儲 uintptr 值的內(nèi)存地址在Go發(fā)生GC時會被回收。而 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)的事實:結(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 起始地址的偏移量, 包括可能的空洞。

              // 指針運算 uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b)。

              // 和 pb := &x.b 等價
              pb := (*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b)))

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

          上面的寫法盡管很繁瑣,但在這里并不是一件壞事,因為這些功能應(yīng)該很謹(jǐn)慎地使用。不要試圖引入一個uintptr類型的臨時變量,因為它可能會破壞代碼的安全性

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

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

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

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

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

          string和[]byte 在運行時的類型表示為reflect.StringHeaderreflect.SliceHeader

          type SliceHeader struct {
           Data uintptr
           Len  int
           Cap  int
          }

          type StringHeader struct {
           Data uintptr
           Len  int
          }

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

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

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

          func main() {
           := "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 []bytestring {
           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)險性,但是在一些場景下,可以提升代碼的效率。

           參考資料

             


          喜歡明哥文章的同學(xué)
          歡迎長按下圖訂閱!

          ???

          瀏覽 55
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <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>
                  亚洲在线视频免费 | 婷婷综合视频 | 久久恋精品五月天婷婷视频 | 在线免费观看黄色视频 | 波多野结衣一区二区三区中文字幕 |