日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 人文社科 > 生活经验 >内容正文

生活经验

Go 学习笔记(74)— Go 标准库之 unsafe

發布時間:2023/11/28 生活经验 38 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Go 学习笔记(74)— Go 标准库之 unsafe 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

Go 語言自帶的 unsafe 包的高級用法, 顧名思義,unsafe 是不安全的。Go 將其定義為這個包名,也是為了讓我們盡可能地不使用它。不過雖然不安全,它也有優勢,那就是可以繞過 Go 的內存安全機制,直接對內存進行讀寫。所以有時候出于性能需要,還是會冒險使用它來對內存進行操作。

1. 指針類型轉換

Go 是一門強類型的靜態語言。強類型意味著一旦定義了,類型就不能改變;靜態意味著類型檢查在運行前就做了。同時出于安全考慮,Go 語言是不允許兩個指針類型進行轉換的。

我們一般使用 *T 作為一個指針類型,表示一個指向類型 T 變量的指針。為了安全的考慮,兩個不同的指針類型不能相互轉換,比如 *int 不能轉為 *float64

我們來看下面的代碼:

func main() {i:= 10ip:=&ivar fp *float64 = (*float64)(ip)fmt.Println(fp)
}

這個代碼在編譯的時候,會提示

cannot convert ip (type * int) to type * float64

也就是不能進行強制轉型。那如果還是需要轉換呢?這就需要使用 unsafe 包里的 Pointer 了。

unsafe.Pointer 是一種特殊意義的指針,可以表示任意類型的地址,類似 C 語言里的 void* 指針,是全能型的。

正常情況下,*int 無法轉換為 *float64 ,但是通過 unsafe.Pointer 做中轉就可以了。在下面的示例中,通過 unsafe.Pointer*int 轉換為 *float64,并且對新的 *float64 進行 3 倍的乘法操作,你會發現原來變量 i 的值也被改變了,變為 30。

func main() {i := 10ip := &ivar fp *float64 = (*float64)(unsafe.Pointer(ip))*fp = *fp * 3fmt.Println(*ip) // 30
}

說明通過 unsafe.Pointer 這個萬能的指針,我們可以在 *T 之間做任何轉換。那么 unsafe.Pointer 到底是什么?為什么其他類型的指針可以轉換為 unsafe.Pointer 呢?這就要看 unsafe.Pointer 的源代碼定義了,如下所示:

// ArbitraryType is here for the purposes of documentation
// only and is not actually part of the unsafe package. 
// It represents the type of an arbitrary Go expression.
type ArbitraryType int
type Pointer *ArbitraryType

Go 語言官方的注釋,ArbitraryType 可以表示任何類型(這里的 ArbitraryType 僅僅是文檔需要,不用太關注它本身,只要記住可以表示任何類型即可)。 而 unsafe.Pointer 又是 *ArbitraryType ,也就是說 unsafe.Pointer 是任何類型的指針,也就是一個通用型的指針,足以表示任何內存地址。

2. uintptr 指針類型

uintptr 也是一種指針類型,它足夠大,可以表示任何指針。它的類型定義如下所示:

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

既然已經有了 unsafe.Pointer ,為什么還要設計 uintptr 類型呢?這是因為 unsafe.Pointer 不能進行運算,比如不支持 +(加號)運算符操作,但是 uintptr 可以。通過它,可以對指針偏移進行計算,這樣就可以訪問特定的內存,達到對特定內存讀寫的目的,這是真正內存級別的操作。

在下面的代碼中,通過指針偏移修改 struct 結構體內的字段為例,演示 uintptr 的用法。

func main() {p := new(person)//Name是person的第一個字段不用偏移,即可通過指針修改pName := (*string)(unsafe.Pointer(p))*pName = "wohu"//Age并不是person的第一個字段,所以需要進行偏移,這樣才能正確定位到Age字段這塊內存,才可以正確的修改pAge := (*int)(unsafe.Pointer(uintptr(unsafe.Pointer(p)) + unsafe.Offsetof(p.Age)))*pAge = 20fmt.Printf("p is %#v", *p) // p is main.person{Name:"wohu", Age:20}
}type person struct {Name stringAge  int
}

這個示例不是通過直接訪問相應字段的方式對 person 結構體字段賦值,而是通過指針偏移找到相應的內存,然后對內存操作進行賦值。

下面詳細介紹操作步驟。

  1. 先使用 new 函數聲明一個 *person 類型的指針變量 p

  2. 然后把 *person 類型的指針變量 p 通過 unsafe.Pointer ,轉換為 *string 類型的指針變量 pName

  3. 因為 person 這個結構體的第一個字段就是 string 類型的 Name ,所以 pName 這個指針就指向 Name 字段(偏移為 0),對 pName 進行修改其實就是修改字段 Name 的值。

  4. 因為 Age 字段不是 person 的第一個字段,要修改它必須要進行指針偏移運算。所以需要先把指針變量 p 通過 unsafe.Pointer 轉換為 uintptr,這樣才能進行地址運算。

既然要進行指針偏移,那么要偏移多少呢?這個偏移量可以通過函數 unsafe.Offsetof 計算出來,該函數返回的是一個 uintptr 類型的偏移量,有了這個偏移量就可以通過 + 號運算符獲得正確的 Age 字段的內存地址了,也就是通過 unsafe.Pointer 轉換后的 *int 類型的指針變量 pAge

然后需要注意的是,如果要進行指針運算,要先通過 unsafe.Pointer 轉換為 uintptr 類型的指針。指針運算完畢后,還要通過 unsafe.Pointer 轉換為真實的指針類型(比如示例中的 *int 類型),這樣可以對這塊內存進行賦值或取值操作。

  1. 有了指向字段 Age 的指針變量 pAge,就可以對其進行賦值操作,修改字段 Age 的值了。

這個示例主要是為了講解 uintptr 指針運算,所以一個結構體字段的賦值才會寫得這么復雜,如果按照正常的編碼,以上示例代碼會和下面的代碼結果一樣。

func main() {p :=new(person)p.Name = "wohu"p.Age = 20fmt.Println(*p)
}

指針運算的核心在于它操作的是一個個內存地址,通過內存地址的增減,就可以指向一塊塊不同的內存并對其進行操作,而且不必知道這塊內存被起了什么名字(變量名)。

3. 指針轉換規則

你已經知道 Go 語言中存在三種類型的指針,它們分別是:常用的 *Tunsafe.Pointeruintptr 。通過以上示例講解,可以總結出這三者的轉換規則:

  • 任何類型的 *T 都可以轉換為 unsafe.Pointer
  • unsafe.Pointer 也可以轉換為任何類型的 *T
  • unsafe.Pointer 可以轉換為 uintptr
  • uintptr 也可以轉換為 unsafe.Pointer

可以發現,unsafe.Pointer 主要用于指針類型的轉換,而且是各個指針類型轉換的橋梁。uintptr 主要用于指針運算,尤其是通過偏移量定位不同的內存。

4. unsafe.Sizeof

Sizeof 函數可以返回一個類型所占用的內存大小,這個大小只與類型有關,和類型對應的變量存儲的內容大小無關,比如 bool 型占用一個字節、int8 也占用一個字節。

通過 Sizeof 函數你可以查看任何類型(比如字符串、切片、整型)占用的內存大小,示例代碼如下:

func main() {fmt.Println(unsafe.Sizeof(true))                 // 1fmt.Println(unsafe.Sizeof(int8(0)))              // 1fmt.Println(unsafe.Sizeof(int16(0)))             // 2fmt.Println(unsafe.Sizeof(int32(0)))             // 4fmt.Println(unsafe.Sizeof(int64(0)))             // 8fmt.Println(unsafe.Sizeof(int(0)))               // 8fmt.Println(unsafe.Sizeof(string("張三")))         // 16fmt.Println(unsafe.Sizeof([]string{"李四", "張三"})) // 24
}

對于整型來說,占用的字節數意味著這個類型存儲數字范圍的大小,比如 int8 占用一個字節,也就是 8bit,所以它可以存儲的大小范圍是 -128~~127,也就是 ?2^(n-1)2^(n-1)?1 。其中 n 表示 bitint8 表示 8bitint16 表示 16bit,以此類推。

對于和平臺有關的 int 類型,要看平臺是 32 位還是 64 位,會取最大的。比如我自己測試以上輸出,會發現 intint64 的大小是一樣的,因為我用的是 64 位平臺的電腦。

小提示:一個 struct 結構體的內存占用大小,等于它包含的字段類型內存占用大小之和。

總結:
unsafe 包里最常用的就是 Pointer 指針,通過它可以讓你在 *TuintptrPointer 三者間轉換,從而實現自己的需求,比如零內存拷貝或通過 uintptr 進行指針運算,這些都可以提高程序效率。

unsafe 包里的功能雖然不安全,但的確很香,比如指針運算、類型轉換等,都可以幫助我們提高性能。不過我還是建議盡可能地不使用,因為它可以繞開 Go 語言編譯器的檢查,可能會因為你的操作失誤而出現問題。當然如果是需要提高性能的必要操作,還是可以使用,比如 []bytestring,就可以通過 unsafe.Pointer 實現零內存拷貝。

5. uintptr 和 unsafe.Pointer 的區別

  • unsafe.Pointer 只是單純的通用指針類型,用于轉換不同類型指針,它不可以參與指針運算;
  • uintptr 是用于指針運算的,GC 不把 uintptr 當指針,也就是說 uintptr 無法持有對象, uintptr 類型的目標會被回收;
  • unsafe.Pointer 可以和 普通指針 進行相互轉換;
  • unsafe.Pointer 可以和 uintptr 進行相互轉換;
package mainimport ("fmt""unsafe"
)type W struct {b int32c int64
}func main() {var w *W = new(W)//這時w的變量打印出來都是默認值0,0fmt.Println(w.b, w.c)//現在我們通過指針運算給b變量賦值為10b := unsafe.Pointer(uintptr(unsafe.Pointer(w)) + unsafe.Offsetof(w.b))*((*int)(b)) = 10//此時結果就變成了10,0fmt.Println(w.b, w.c)
}
  • uintptr(unsafe.Pointer(w)) 獲取了 w 的指針起始值;
  • unsafe.Offsetof(w.b) 獲取 b 變量的偏移量;
  • 兩個相加就得到了 b 的地址值,將通用指針 Pointer 轉換成具體指針 ((*int)(b)),通過 *符號取值,然后賦值。*((*int)(b)) 相當于把 (*int)(b) 轉換成 int 了,最后對變量重新賦值成 10,這樣指針運算就完成了。

總結

以上是生活随笔為你收集整理的Go 学习笔记(74)— Go 标准库之 unsafe的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。