有點不安全卻又一亮的 Go unsafe.Pointer

在上一篇文章 《深刻理解 Go Slice》 中,你們會發現其底層數據結構使用了 unsafe.Pointer。所以想着再介紹一下其關聯知識git

原文地址:有點不安全卻又一亮的 Go unsafe.Pointergithub

前言

在你們學習 Go 的時候,確定都學過 「Go 的指針是不支持指針運算和轉換」 這個知識點。爲何呢?golang

首先,Go 是一門靜態語言,全部的變量都必須爲標量類型。不一樣的類型不可以進行賦值、計算等跨類型的操做。那麼指針也對應着相對的類型,也在 Compile 的靜態類型檢查的範圍內。同時靜態語言,也稱爲強類型。也就是一旦定義了,就不能再改變它segmentfault

錯誤示例

func main(){
    num := 5
    numPointer := &num

    flnum := (*float32)(numPointer)
    fmt.Println(flnum)
}

輸出結果:安全

# command-line-arguments
...: cannot convert numPointer (type *int) to type *float32

在示例中,咱們建立了一個 num 變量,值爲 5,類型爲 int。取了其對於的指針地址後,試圖強制轉換爲 *float32,結果失敗...數據結構

unsafe

針對剛剛的 「錯誤示例」,咱們能夠採用今天的男主角 unsafe 標準庫來解決。它是一個神奇的包,在官方的詮釋中,有以下概述:學習

  • 圍繞 Go 程序內存安全及類型的操做
  • 極可能會是不可移植的
  • 不受 Go 1 兼容性指南的保護

簡單來說就是,不怎麼推薦你使用。由於它是 unsafe(不安全的),可是在特殊的場景下,使用了它。能夠打破 Go 的類型和內存安全機制,讓你得到眼前一亮的驚喜效果 😄ui

Pointer

爲了解決這個問題,須要用到 unsafe.Pointer。它表示任意類型且可尋址的指針值,能夠在不一樣的指針類型之間進行轉換(相似 C 語言的 void * 的用途)指針

其包含四種核心操做:code

  • 任何類型的指針值均可以轉換爲 Pointer
  • Pointer 能夠轉換爲任何類型的指針值
  • uintptr 能夠轉換爲 Pointer
  • Pointer 能夠轉換爲 uintptr

在這一部分,重點看第一點、第二點。你再想一想怎麼修改 「錯誤示例」 讓它運行起來?

func main(){
    num := 5
    numPointer := &num

    flnum := (*float32)(unsafe.Pointer(numPointer))
    fmt.Println(flnum)
}

輸出結果:

0xc4200140b0

在上述代碼中,咱們小加改動。經過 unsafe.Pointer 的特性對該指針變量進行了修改,就能夠完成任意類型(*T)的指針轉換

須要注意的是,這時還沒法對變量進行操做或訪問。由於不知道該指針地址指向的東西具體是什麼類型。不知道是什麼類型,又如何進行解析呢。沒法解析也就天然沒法對其變動了

Offsetof

在上小節中,咱們對普通的指針變量進行了修改。那麼它是否能作更復雜一點的事呢?

type Num struct{
    i string
    j int64
}

func main(){
    n := Num{i: "EDDYCJY", j: 1}
    nPointer := unsafe.Pointer(&n)

    niPointer := (*string)(unsafe.Pointer(nPointer))
    *niPointer = "煎魚"

    njPointer := (*int64)(unsafe.Pointer(uintptr(nPointer) + unsafe.Offsetof(n.j)))
    *njPointer = 2

    fmt.Printf("n.i: %s, n.j: %d", n.i, n.j)
}

輸出結果:

n.i: 煎魚, n.j: 2

在剖析這段代碼作了什麼事以前,咱們須要瞭解結構體的一些基本概念:

  • 結構體的成員變量在內存存儲上是一段連續的內存
  • 結構體的初始地址就是第一個成員變量的內存地址
  • 基於結構體的成員地址去計算偏移量。就可以得出其餘成員變量的內存地址

再回來看看上述代碼,得出執行流程:

  • 修改 n.i 值:i 爲第一個成員變量。所以不須要進行偏移量計算,直接取出指針後轉換爲 Pointer,再強制轉換爲字符串類型的指針值便可
  • 修改 n.j 值:j 爲第二個成員變量。須要進行偏移量計算,才能夠對其內存地址進行修改。在進行了偏移運算後,當前地址已經指向第二個成員變量。接着重複轉換賦值便可

須要注意的是,這裏使用了以下方法(來完成偏移計算的目標):

一、uintptr:uintptr 是 Go 的內置類型。返回無符號整數,可存儲一個完整的地址。後續經常使用於指針運算

type uintptr uintptr

二、unsafe.Offsetof:返回成員變量 x 在結構體當中的偏移量。更具體的講,就是返回結構體初始位置到 x 之間的字節數。須要注意的是入參 ArbitraryType 表示任意類型,並不是定義的 int。它實際做用是一個佔位符

func Offsetof(x ArbitraryType) uintptr

在這一部分,其實就是巧用了 Pointer 的第3、第四點特性。這時候就已經能夠對變量進行操做了 😄

錯誤示例

func main(){
    n := Num{i: "EDDYCJY", j: 1}
    nPointer := unsafe.Pointer(&n)
    ...

    ptr := uintptr(nPointer)
    njPointer := (*int64)(unsafe.Pointer(ptr + unsafe.Offsetof(n.j)))
    ...
}

這裏存在一個問題,uintptr 類型是不能存儲在臨時變量中的。由於從 GC 的角度來看,uintptr 類型的臨時變量只是一個無符號整數,並不知道它是一個指針地址

所以當知足必定條件後,ptr 這個臨時變量是可能被垃圾回收掉的,那麼接下來的內存操做,豈不成迷?

總結

簡潔回顧兩個知識點。第一是 unsafe.Pointer 可讓你的變量在不一樣的指針類型轉來轉去,也就是表示爲任意可尋址的指針類型。第二是 uintptr 經常使用於與 unsafe.Pointer 打配合,用於作指針運算,巧妙地很

最後仍是那句,沒有特殊必要的話。是不建議使用 unsafe 標準庫,它並不安全。雖然它經常能讓你眼前一亮 👌

相關文章
相關標籤/搜索