標準庫unsafe:帶你突破golang中的型別限制

华为云开发者联盟發表於2024-03-29

本文分享自華為雲社群《突破語言golang中的型別限制》,作者:碼樂。

1 簡介

在使用c語言程式設計時,常常因為型別的問題大傷腦筋,而其他語言比如java,python預設型別又是難以改變的,golang提供了一些方式用於喜歡hack的使用者。

2 標準庫unsafe的簡單介紹

官方說明標準庫 unsafe 包含繞過 Go 程式的型別安全的操作。

匯入unsafe包可能是不可移植的,並且不受 Go 1 相容性指南的保護。

在1.20中,標準庫的unsafe包很小, 二個結構體型別,八個函式,在一個檔案中。

    package unsage

    type ArbitraryType int
    type IntegerType int
    type Pointer *ArbitraryType

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

    func Add(ptr Pointer, len IntegerType) Pointer
    func Slice(ptr *ArbitraryType, len IntegerType) []ArbitraryType
    func SliceData(slice []ArbitraryType) *ArbitraryType
    func String(ptr *byte, len IntegerType) string
    func StringData(str string) *byte

unsafe包定義了 二個型別和 八個函式,二個型別 ArbitraryType 和 IntegerType 不真正屬於unsafe包,我們在Go程式碼中並不能使用它們定義變數。

它表示一個任意表示式的型別,僅用於文件目的,Go編譯器會對其做特殊處理。

雖然位於 unsafe,但是 Alignof,Offsetof,Sizeof,這三個函式的使用是絕對安全的。 以至於Go設計者Rob pike提議移走它們。

這三個函式的共同點是 都返回 uintptr 型別。

之所以使用 uintptr 型別而不是 uint64 整型,因為這三個函式更多應用於 有 unsafe.Pointer和 uintptr型別引數的指標運算。

採用uintptr做為返回值型別可以減少指標運算表示式的顯式型別轉換。

2.1 獲取大小 Sizeof

Sizeof 用於獲取一個表示式的大小。 該函式獲取一個任意型別的表示式 x,並返回 按bytes計算 的大小,假設變數v,並且v透過 v =x宣告。

Sizeof 接收任何型別的表示式x,並返回以bytes位元組為單位的大小, 並且假設變數v是透過var v = x宣告的。該大小不包括任何可能被x引用的記憶體。

例如,如果x是一個切片,Sizeof返回切片描述符的大小,而不是該片所引用的記憶體的大小。
對於一個結構體,其大小包括由欄位對齊引入的任何填充。

如果引數x的型別沒有變化,不具有可變的大小,Sizeof的返回值是一個Go常數不可變值 。
(如果一個型別是一個型別引數,或者是一個陣列,則該型別具有可變的大小或結構型別中的元素大小可變)。

示例:

    var (
        i  int = 5
        a      = [10]int{}
        ss     = a[:]
        f  FuncFoo

        preValue = map[string]uintptr{
            "i":       8,
            "a":       80,
            "ss":      24,
            "f":       48,
            "f.c":     10,
            "int_nil": 8,
        }
    )

    type FuncFoo struct {
        a int
        b string
        c [10]byte
        d float64
    }
    func TestFuncSizeof(t *testing.T) {
        defer setUp(t.Name())()
        fmt.Printf("\tExecute test:%v\n", t.Name())

        if unsafe.Sizeof(i) != preValue["i"] {
            ErrorHandler(fmt.Sprintf("size: %v not equal %v", unsafe.Sizeof(i), preValue["i"]), t)
        }

        if unsafe.Sizeof(a) != preValue["a"] {
            ErrorHandler(fmt.Sprintf("size: %v not equal %v", unsafe.Sizeof(i), preValue["a"]), t)

        }

        if unsafe.Sizeof(ss) != preValue["ss"] {
            ErrorHandler(fmt.Sprintf("size: %v not equal %v", unsafe.Sizeof(i), preValue["ss"]), t)

        }
        if unsafe.Sizeof(f) != preValue["f"] {
            ErrorHandler(fmt.Sprintf("size: %v not equal %v", unsafe.Sizeof(i), preValue["f"]), t)

        }
        if unsafe.Sizeof(f.c) != preValue["f.c"] {
            ErrorHandler(fmt.Sprintf("size: %v not equal %v", unsafe.Sizeof(i), preValue["f.c"]), t)

        }
        if unsafe.Sizeof(unsafe.Sizeof((*int)(nil))) != preValue["int_nil"] {
            ErrorHandler(fmt.Sprintf("size: %v not equal %v", unsafe.Sizeof(i), preValue["int_nil"]), t)

        }
    }

Sizeof 函式不支援之間傳入無型別資訊的nil值,如下錯誤

    unsafe.Sizeof(nil)  

我們必須顯式告知 Sizeof 傳入的nil究竟是那個型別,

    unsafe.Sizeof(unsafe.Sizeof((*int)(nil))) 

必須顯式告知nil是哪個型別的nil,這就是傳入一個值 nil 但是型別明確的變數。

對齊係數 Alignof 用於獲取一個表示式的內地地址對齊係數,對齊係數 alignment factor 是一個計算機體系架構 computer architecture 層面的術語。

在不同計算機體系中,處理器對變數地址都有對齊要求,即變數的地址必須可被該變數的對齊係數整除。

它接收一個任何型別的表示式x,並返回所需的排列方式 假設變數v是透過var v = x宣告的。
它是m一個最大的值。

例1,

        a      = [10]int{}

        reflect.TypeOf(x).Align()  //8
        unsafe.Alignof(a)   //8

它與reflect.TypeOf(x).Align()返回的值相同。

作為一個特例,如果一個變數s是結構型別,f是一個欄位,那麼Alignof(s.f)將返回所需的對齊方式。

該型別的欄位在結構中的位置。這種情況與reeflect.TypeOf(s.f).FieldAlign()返回的值。

Alignof的返回值是一個Go常數,如果引數的型別不具有可變大小。
(關於可變大小型別的定義,請參見[Sizeof]的描述)。

繼上 例2:

      var (
        i  int = 5
        a      = [10]int{}
        ss     = a[:]
        f  FuncFoo
        zhs = ""

        preValue = map[string]uintptr{
            "i":       8,
            "a":       80,
            "ss":      24,
            "f":       48,
            "f.c":     10,
            "int_nil": 8,
        }
    )

    func TestAlignof(t *testing.T) {

        defer setUp(t.Name())()
        fmt.Printf("\tExecute test:%v\n", t.Name())

        var x int 

        b := uintptr(unsafe.Pointer(&x))%unsafe.Alignof(x) == 0
        t.Log("alignof:", b)

        if unsafe.Alignof(i) != preValue["i"] {
            ErrorHandler(fmt.Sprintf("Alignof: %v not equal %v", unsafe.Sizeof(i), preValue["int_nil"]), t)

        }

        if unsafe.Alignof(a) != preValue["i"] {
            ErrorHandler(fmt.Sprintf("Alignof: %v not equal %v", unsafe.Sizeof(i), preValue["int_nil"]), t)

        }

        if unsafe.Alignof(ss) != preValue["i"] {
            ErrorHandler(fmt.Sprintf("Alignof: %v not equal %v", unsafe.Sizeof(i), preValue["int_nil"]), t)

        }

        if unsafe.Alignof(f.a) != preValue["i"] {
            ErrorHandler(fmt.Sprintf("Alignof: %v not equal %v", unsafe.Sizeof(i), preValue["int_nil"]), t)

        }

        if unsafe.Alignof(f) != preValue["i"] {
            ErrorHandler(fmt.Sprintf("Alignof: %v not equal %v", unsafe.Sizeof(i), preValue["int_nil"]), t)

        }

中文對齊係數 為 8

        if unsafe.Alignof(zhs) != preValue["i"] {
            ErrorHandler(fmt.Sprintf("Alignof: %v not equal %v", unsafe.Sizeof(i), preValue["i"]), t)
        }

空結構體對齊係數 1

        if unsafe.Alignof(struct{}{}) != 1 {
            ErrorHandler(fmt.Sprintf("Alignof: %v not equal %v", unsafe.Sizeof(i), 1), t)
        }

byte 陣列對齊係數為 1

        if unsafe.Alignof(sbyte) != 1 {
            ErrorHandler(fmt.Sprintf("Alignof: %v not equal %v", unsafe.Sizeof(i), 1), t)
        }

長度為0 的陣列,與其元素的對齊係數相同

        if unsafe.Alignof([0]int{}) != 8 {
            ErrorHandler(fmt.Sprintf("Alignof: %v not equal %v", unsafe.Sizeof(i), 8), t)
        }

長度為0 的陣列,與其元素的對齊係數相同

        if unsafe.Alignof([0]struct{}{}) != 1 {
            ErrorHandler(fmt.Sprintf("Alignof: %v not equal %v", unsafe.Sizeof(i), 1), t)
        }

    }

執行它:

    go test -timeout 30s -run ^TestAlignof$ ./unsafe_case.go

對齊係數 alignment factor,變數的地址必須可被該變數的對齊係數整除。

2.2 使用對齊的例子

我們使用相同欄位,分別建立兩個結構體屬性分別為對齊或不對齊,幫助 go 更好地分配記憶體和 使用cpu讀取,檢視效果

    type RandomResource struct {
        Cloud               string // 16 bytes
        Name                string // 16 bytes
        HaveDSL             bool   //  1 byte
        PluginVersion       string // 16 bytes
        IsVersionControlled bool   //  1 byte
        TerraformVersion    string // 16 bytes
        ModuleVersionMajor  int32  //  4 bytes
    }

    type OrderResource struct {
        ModuleVersionMajor  int32  //  4 bytes
        HaveDSL             bool   //  1 byte
        IsVersionControlled bool   //  1 byte
        Cloud               string // 16 bytes
        Name                string // 16 bytes
        PluginVersion       string // 16 bytes
        TerraformVersion    string // 16 bytes

    }

欄位 儲存使用的空間與 欄位值沒有關係

         var d RandomResource
         d.Cloud = "aws-singapore"
         ...

         InfoHandler(fmt.Sprintf("隨機順序屬性的結構體記憶體 總共佔用 StructType: %T => [%d]\n", d, unsafe.Sizeof(d)), m)
         var te = OrderResource{}
         te.Cloud = "aws-singapore"  
         ...
         m.Logf("屬性對齊的結構體記憶體 總共佔用  StructType:d %T => [%d]\n", te, unsafe.Sizeof(te))

然後複製結構體,並改變其屬性值,檢視儲存空間和值的長度變化

        te2 := te
        te2.Cloud = "ali2"
        m.Logf("結構體2 te2:%#v\n", &te2)
        m.Logf("結構體1 te:%#v\n", &te)

        m.Log("改變 te3 將同時改變 te,te3 指向了 te的地址")
        m.Log("複製了對齊結構體,並重新賦值,用於檢視欄位長度。")
        m.Log("(*te).Cloud:", (te).Cloud, "*te.Cloud", te.Cloud, "te size:", unsafe.Sizeof(te.Cloud), "te value len:", len(te.Cloud))

        te3 := &te
        te3.Cloud = "HWCloud2"

        m.Log("(*te3).Cloud:", (*te3).Cloud, "*te3.Cloud", te3.Cloud, "te3 size:", unsafe.Sizeof(te3.Cloud), "te3 value len:", len(te3.Cloud))
        m.Logf("欄位 Cloud:%v te3:%p\n", (*te3).Cloud, te3)
        m.Logf("欄位 Cloud:%v order:%v te:%v, addr:%p\n", te.Cloud, (te).Cloud, te, &te)

執行它,

    go test -v .\case_test.go

得到以下輸出:

隨機順序屬性的結構體記憶體 總共佔用 StructType: main.Raesource => [88]

    ...

屬性對齊的結構體記憶體 總共佔用 StructType:d main.OrderResource => [72]

改變 te3 將同時改變 te,te3 指向了 te的地址

    case_test.go:186: 複製了對齊結構體,並重新賦值,用於檢視欄位長度。

    case_test.go:188: (*te).Cloud: aws-singapore *te.Cloud aws-singapore te size: 16 te Alignof: 8 te value len: 13 reflect Align len and field Align len: 8 8
    case_test.go:190: (*te2).Cloud: ali2 *te2.Cloud aws-singapore te2 size: 16 te2 Alignof: 8 te2 value len: 4 reflect Align len and field Align len: 8 8
    case_test.go:196: (*te3).Cloud: HWCloud2-asia-southeast-from-big-plant-place-air-local-video-service-picture-merge-from-other-all-company *te3.Cloud HWCloud2-asia-southeast-from-big-plant-place-air-local-video-service-picture-merge-from-other-all-company te3 
size: 16 te3 Alignof: 8 te3 value len: 105 reflect Align len and field Align len: 8 8

    case_test.go: 結構體1欄位 Cloud:HWCloud2-asia-southeast-from-big-plant-place-air-local-video-service-picture-merge-from-other-all-company te2:0xc0000621e0
    case_test.go:198: 結構體2欄位 Cloud:ali2 te2:0xc000062280
    case_test.go:199: 結構體3欄位 Cloud:HWCloud2-asia-southeast-from-big-plant-place-air-local-video-service-picture-merge-from-other-all-company te3:0xc0000621e0

小結

我們介紹了unsafe包的檢查功能,在初始化時,go結構體已經分配了對於的記憶體空間,

一個結構體而言,結構體屬性為隨機順序的,go將分配更多記憶體空間。 即使是複製後。

比如 結構體的Cloud 欄位。

Sizeof表示式大小總是16,
而對齊係數 Alignof 大小總是8,
而在不同的結構體例項中值長度可以為 4,13, 105.

本節原始碼地址:

https://github.com/hahamx/examples/tree/main/alg_practice/2_sys_io

點選關注,第一時間瞭解華為雲新鮮技術~

相關文章