[轉]Go裏面的unsafe包詳解

Golang的unsafe包是一個很特殊的包。 爲何這樣說呢? 本文將詳細解釋。程序員

來自go語言官方文檔的警告

unsafe包的文檔是這麼說的:golang

導入unsafe的軟件包可能不可移植,而且不受Go 1兼容性指南的保護。

Go 1 兼容性指南這麼說:express

導入unsafe軟件包可能取決於Go實現的內部屬性。 咱們保留對可能致使程序崩潰的實現進行更改的權利。

固然包名稱暗示unsafe包是不安全的。 但這個包有多危險呢? 讓咱們先看看unsafe包的做用。數組

Unsafe包的做用

直到如今(Go1.7),unsafe包含如下資源:安全

  • 三個函數:app

    • func Alignof(variable ArbitraryType)uintptr
    • func Offsetof(selector ArbitraryType)uintptr
    • func Sizeof(variable ArbitraryType)uintptr
  • 和一種類型:函數

    • 類型Pointer * ArbitraryType

這裏,ArbitraryType不是一個真正的類型,它只是一個佔位符。佈局

與Golang中的大多數函數不一樣,上述三個函數的調用將始終在編譯時求值,而不是運行時。 這意味着它們的返回結果能夠分配給常量。ui

(BTW,unsafe包中的函數中非惟一調用將在編譯時求值。當傳遞給len和cap的參數是一個數組值時,內置函數和cap函數的調用也能夠在編譯時被求值。)this

除了這三個函數和一個類型外,指針在unsafe包也爲編譯器服務。

出於安全緣由,Golang不容許如下之間的直接轉換:

  • 兩個不一樣指針類型的值,例如 int64和 float64。

  • 指針類型和uintptr的值。

可是藉助unsafe.Pointer,咱們能夠打破Go類型和內存安全性,並使上面的轉換成爲可能。這怎麼可能發生?讓咱們閱讀unsafe包文檔中列出的規則:

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

這些規則與Go規範一致:

底層類型uintptr的任何指針或值均可以轉換爲指針類型,反之亦然。

規則代表unsafe.Pointer相似於c語言中的void 。固然,void 在C語言裏是危險的!

在上述規則下,對於兩種不一樣類型T1和T2,可使 T1值與unsafe.Pointer值一致,而後將unsafe.Pointer值轉換爲 T2值(或uintptr值)。經過這種方式能夠繞過Go類型系統和內存安全性。固然,濫用這種方式是很危險的。

舉個例子:

package main

import (
    "fmt"
    "unsafe"
)
func main() {
    var n int64 = 5
    var pn = &n
    var pf = (*float64)(unsafe.Pointer(pn))
    // now, pn and pf are pointing at the same memory address
    fmt.Println(*pf) // 2.5e-323
    *pf = 3.14159
    fmt.Println(n) // 4614256650576692846
}

  

在這個例子中的轉換多是無心義的,但它是安全和合法的(爲何它是安全的?)。

所以,資源在unsafe包中的做用是爲Go編譯器服務,unsafe.Pointer類型的做用是繞過Go類型系統和內存安全。

再來一點 unsafe.Pointer 和 uintptr

這裏有一些關於unsafe.Pointer和uintptr的事實:

  • uintptr是一個整數類型。
    • 即便uintptr變量仍然有效,由uintptr變量表示的地址處的數據也可能被GC回收。
  • unsafe.Pointer是一個指針類型。
    • 可是unsafe.Pointer值不能被取消引用。
    • 若是unsafe.Pointer變量仍然有效,則由unsafe.Pointer變量表示的地址處的數據不會被GC回收。
    • unsafe.Pointer是一個通用的指針類型,就像* int等。

因爲uintptr是一個整數類型,uintptr值能夠進行算術運算。 因此經過使用uintptr和unsafe.Pointer,咱們能夠繞過限制,* T值不能在Golang中計算偏移量:

package main

import (
    "fmt"
    "unsafe"
)

func main() {
    a := [4]int{0, 1, 2, 3}
    p1 := unsafe.Pointer(&a[1])
    p3 := unsafe.Pointer(uintptr(p1) + 2 * unsafe.Sizeof(a[0]))
    *(*int)(p3) = 6
    fmt.Println("a =", a) // a = [0 1 2 6]

    // ...

    type Person struct {
        name   string
        age    int
        gender bool
    }

    who := Person{"John", 30, true}
    pp := unsafe.Pointer(&who)
    pname := (*string)(unsafe.Pointer(uintptr(pp) + unsafe.Offsetof(who.name)))
    page := (*int)(unsafe.Pointer(uintptr(pp) + unsafe.Offsetof(who.age)))
    pgender := (*bool)(unsafe.Pointer(uintptr(pp) + unsafe.Offsetof(who.gender)))
    *pname = "Alice"
    *page = 28
    *pgender = false
    fmt.Println(who) // {Alice 28 false}
}

  

unsafe包有多危險

關於unsafe包,Ian,Go團隊的核心成員之一,已經確認:

  • 在unsafe包中的函數的簽名將不會在之後的Go版本中更改,

  • 而且unsafe.Pointer類型將在之後的Go版本中始終存在。

因此,unsafe包中的三個函數看起來不危險。 go team leader甚至想把它們放在別的地方。 unsafe包中這幾個函數惟一不安全的是它們調用結果可能在後來的版本中返回不一樣的值。 很難說這種不安全是一種危險。

看起來全部的unsafe包的危險都與使用unsafe.Pointer有關。 unsafe包docs列出了一些使用unsafe.Pointer合法或非法的狀況。 這裏只列出部分非法使用案例:

package main

import (
    "fmt"
    "unsafe"
)

// case A: conversions between unsafe.Pointer and uintptr 
//         don't appear in the same expression
func illegalUseA() {
    fmt.Println("===================== illegalUseA")

    pa := new([4]int)

    // split the legal use
    // p1 := unsafe.Pointer(uintptr(unsafe.Pointer(pa)) + unsafe.Sizeof(pa[0]))
    // into two expressions (illegal use):
    ptr := uintptr(unsafe.Pointer(pa))
    p1 := unsafe.Pointer(ptr + unsafe.Sizeof(pa[0]))
    // "go vet" will make a warning for the above line:
    // possible misuse of unsafe.Pointer

    // the unsafe package docs, https://golang.org/pkg/unsafe/#Pointer,
    // thinks above splitting is illegal.
    // but the current Go compiler and runtime (1.7.3) can't detect
    // this illegal use.
    // however, to make your program run well for later Go versions,
    // it is best to comply with the unsafe package docs.

    *(*int)(p1) = 123
    fmt.Println("*(*int)(p1)  :", *(*int)(p1)) //
}    

// case B: pointers are pointing at unknown addresses
func illegalUseB() {
    fmt.Println("===================== illegalUseB")

    a := [4]int{0, 1, 2, 3}
    p := unsafe.Pointer(&a)
    p = unsafe.Pointer(uintptr(p) + uintptr(len(a)) * unsafe.Sizeof(a[0]))
    // now p is pointing at the end of the memory occupied by value a.
    // up to now, although p is invalid, it is no problem.
    // but it is illegal if we modify the value pointed by p
    *(*int)(p) = 123
    fmt.Println("*(*int)(p)  :", *(*int)(p)) // 123 or not 123
    // the current Go compiler/runtime (1.7.3) and "go vet" 
    // will not detect the illegal use here.

    // however, the current Go runtime (1.7.3) will 
    // detect the illegal use and panic for the below code.
    p = unsafe.Pointer(&a)
    for i := 0; i <= len(a); i++ {
        *(*int)(p) = 123 // Go runtime (1.7.3) never panic here in the tests

        fmt.Println(i, ":", *(*int)(p))
        // panic at the above line for the last iteration, when i==4.
        // runtime error: invalid memory address or nil pointer dereference

        p = unsafe.Pointer(uintptr(p) + unsafe.Sizeof(a[0]))
    }
}

func main() {
    illegalUseA()
    illegalUseB()
}

  

編譯器很難檢測Go程序中非法的unsafe.Pointer使用。 運行「go vet」能夠幫助找到一些潛在的錯誤,但不是全部的都能找到。 一樣是Go運行時,也不能檢測全部的非法使用。 非法unsafe.Pointer使用可能會使程序崩潰或表現得怪異(有時是正常的,有時是異常的)。 這就是爲何使用不安全的包是危險的。

轉換T1 爲 T2

對於將 T1轉換爲unsafe.Pointer,而後轉換爲 T2,unsafe包docs說:

若是T2比T1大,而且二者共享等效內存佈局,則該轉換容許將一種類型的數據從新解釋爲另外一類型的數據。

這種「等效內存佈局」的定義是有一些模糊的。 看起來go團隊故意如此。 這使得使用unsafe包更危險。

因爲Go團隊不肯意在這裏作出準確的定義,本文也不嘗試這樣作。 這裏,列出了已確認的合法用例的一小部分,

合法用例1:在[]T和[]MyT之間轉換

在這個例子裏,咱們用int做爲T:

type MyInt int

在Golang中,[] int和[] MyInt是兩種不一樣的類型,它們的底層類型是自身。 所以,[] int的值不能轉換爲[] MyInt,反之亦然。 可是在unsafe.Pointer的幫助下,轉換是可能的:

package main

import (
    "fmt"
    "unsafe"
)

func main() {
    type MyInt int

    a := []MyInt{0, 1, 2}
    // b := ([]int)(a) // error: cannot convert a (type []MyInt) to type []int
    b := *(*[]int)(unsafe.Pointer(&a))

    b[0]= 3

    fmt.Println("a =", a) // a = [3 1 2]
    fmt.Println("b =", b) // b = [3 1 2]

    a[2] = 9

    fmt.Println("a =", a) // a = [3 1 9]
    fmt.Println("b =", b) // b = [3 1 9]
}

  

合法用例2: 調用sync/atomic包中指針相關的函數

sync / atomic包中的如下函數的大多數參數和結果類型都是unsafe.Pointer或*unsafe.Pointer:

  • func CompareAndSwapPointer(addr * unsafe.Pointer,old,new unsafe.Pointer)(swapped bool)
  • func LoadPointer(addr * unsafe.Pointer)(val unsafe.Pointer)
  • func StorePointer(addr * unsafe.Pointer,val unsafe.Pointer)
  • func SwapPointer(addr * unsafe.Pointer,new unsafe.Pointer)(old unsafe.Pointer)

要使用這些功能,必須導入unsafe包。 注意: unsafe.Pointer是通常類型,所以 unsafe.Pointer的值能夠轉換爲unsafe.Pointer,反之亦然。

package main

import (
    "fmt"
    "log"
    "time"
    "unsafe"
    "sync/atomic"
    "sync"
    "math/rand"
)

var data *string

// get data atomically
func Data() string {
    p := (*string)(atomic.LoadPointer(
            (*unsafe.Pointer)(unsafe.Pointer(&data)),
        ))
    if p == nil {
        return ""
    } else {
        return *p
    }
}

// set data atomically
func SetData(d string) {
    atomic.StorePointer(
            (*unsafe.Pointer)(unsafe.Pointer(&data)), 
            unsafe.Pointer(&d),
        )
}

func main() {
    var wg sync.WaitGroup
    wg.Add(200)

    for range [100]struct{}{} {
        go func() {
            time.Sleep(time.Second * time.Duration(rand.Intn(1000)) / 1000)

            log.Println(Data())
            wg.Done()
        }()
    }

    for i := range [100]struct{}{} {
        go func(i int) {
            time.Sleep(time.Second * time.Duration(rand.Intn(1000)) / 1000)
            s := fmt.Sprint("#", i)
            log.Println("====", s)

            SetData(s)
            wg.Done()
        }(i)
    }

    wg.Wait()

    fmt.Println("final data = ", *data)
}

  

結論

  • unsafe包用於Go編譯器,而不是Go運行時。
  • 使用unsafe做爲程序包名稱只是讓你在使用此包是更加當心。
  • 使用unsafe.Pointer並不老是一個壞主意,有時咱們必須使用它。
  • Golang的類型系統是爲了安全和效率而設計的。 可是在Go類型系統中,安全性比效率更重要。 一般Go是高效的,但有時安全真的會致使Go程序效率低下。 unsafe包用於有經驗的程序員經過安全地繞過Go類型系統的安全性來消除這些低效。
  • unsafe包可能被濫用而且是危險的。


原文:https://gocn.vip/question/371

相關文章
相關標籤/搜索