關于golang指針的有限操作詳解
傳統(tǒng)意義上來說,指針是一個指向某個確切的內存地址的值。這個內存地址可以是任何數(shù)據(jù)或代碼的起始地址。在Go語言中有幾種東西可以代表"指針"。其中最貼切傳統(tǒng)意義的當屬uintptr
類型的了。該類型實際上是一個數(shù)值類型,也是Go語言內建的數(shù)據(jù)類型之一。
根據(jù)當前計算機的計算架構的不同,它可以存儲32位或64位的無符號整數(shù),可以代表任何指針的位(bit)模式,也就是原始的內存地址。
Go語言標準庫中的unsafe
包,unsafe
包中有一個類型叫做Pointer
,也代表任何指針的位(bit)模式,也就是原始的內存地址。
unsafe.Pointer
可以表示任何指向可尋址的值的指針,同時它也是前面提到的指針值和uintptr
值之間的橋梁。通過它,我們可以在這兩種值之上進行雙向的轉換。這里有一個很關鍵的詞——可尋址的(addressable)
。
在我們繼續(xù)說unsafe.Pointer
之前,需要先搞清楚這個詞的確切含義。
Go語言中的哪些值是不可尋址的?
- 常量的值。
- 基本類型值的字面量。
- 算術操作的結果值。
- 對各種字面量的索引表達式和切片表達式的結果值。
不過有一個例外,對切片字面量的索引結果值卻是可尋址的。
- 對字符串變量的索引表達式和切片表達式的結果值。
- 對字典變量的索引表達式的結果值。
- 函數(shù)字面量和方法字面量,以及對它們的調用表達式的結果值。
- 結構體字面量的字段值,也就是對結構體字面量的選擇表達式的結果值。
- 類型轉換表達式的結果值。
- 類型斷言表達式的結果值。
- 接收表達式的結果值。
// 示例1。 const num = 123 //_ = &num // 常量不可尋址。 //_ = &(123) // 基本類型值的字面量不可尋址。 var str = "abc" _ = str //_ = &(str[0]) // 對字符串變量的索引結果值不可尋址。 //_ = &(str[0:2]) // 對字符串變量的切片結果值不可尋址。 str2 := str[0] _ = &str2 // 但這樣的尋址就是合法的。 //_ = &(123 + 456) // 算術操作的結果值不可尋址。 num2 := 456 _ = num2 //_ = &(num + num2) // 算術操作的結果值不可尋址。 //_ = &([3]int{1, 2, 3}[0]) // 對數(shù)組字面量的索引結果值不可尋址。 //_ = &([3]int{1, 2, 3}[0:2]) // 對數(shù)組字面量的切片結果值不可尋址。 _ = &([]int{1, 2, 3}[0]) // 對切片字面量的索引結果值卻是可尋址的。 //_ = &([]int{1, 2, 3}[0:2]) // 對切片字面量的切片結果值不可尋址。 //_ = &(map[int]string{1: "a"}[0]) // 對字典字面量的索引結果值不可尋址。 var map1 = map[int]string{1: "a", 2: "b", 3: "c"} _ = map1 //_ = &(map1[2]) // 對字典變量的索引結果值不可尋址。 //_ = &(func(x, y int) int { // return x + y //}) // 字面量代表的函數(shù)不可尋址。 //_ = &(fmt.Sprintf) // 標識符代表的函數(shù)不可尋址。 //_ = &(fmt.Sprintln("abc")) // 對函數(shù)的調用結果值不可尋址。 dog := Dog{"little pig"} _ = dog //_ = &(dog.Name) // 標識符代表的函數(shù)不可尋址。 //_ = &(dog.Name()) // 對方法的調用結果值不可尋址。 //_ = &(Dog{"little pig"}.name) // 結構體字面量的字段不可尋址。 //_ = &(interface{}(dog)) // 類型轉換表達式的結果值不可尋址。 dogI := interface{}(dog) _ = dogI //_ = &(dogI.(Named)) // 類型斷言表達式的結果值不可尋址。 named := dogI.(Named) _ = named //_ = &(named.(Dog)) // 類型斷言表達式的結果值不可尋址。 var chan1 = make(chan int, 1) chan1 <- 1 //_ = &(<-chan1) // 接收表達式的結果值不可尋址。
常量的值總是會被存儲到一個確切的內存區(qū)域中,并且這種值肯定是不可變的
?;绢愋椭档淖置媪恳彩且粯?,其實它們本就可以被視為常量,只不過沒有任何標識符可以代表它們罷了。
第一個關鍵詞:不可變的。由于 Go 語言中的字符串值也是不可變的,所以對于一個字符串類型的變量來說,基于它的索引或切片的結果值也都是不可尋址的,因為即使拿到了這種值的內存地址也改變不了什么。
算術操作的結果值屬于一種臨時結果
。在我們把這種結果值賦給任何變量或常量之前,即使能拿到它的內存地址也是沒有任何意義的。
第二個關鍵詞:臨時結果。這個關鍵詞能被用來解釋很多現(xiàn)象。我們可以把各種對值字面量施加的表達式的求值結果都看做是臨時結果。
我們都知道,Go 語言中的表達式有很多種,其中常用的包括以下幾種。
- 用于獲得某個元素的索引表達式。
- 用于獲得某個切片(片段)的切片表達式。
- 用于訪問某個字段的選擇表達式。
- 用于調用某個函數(shù)或方法的調用表達式。
- 用于轉換值的類型的類型轉換表達式。
- 用于判斷值的類型的類型斷言表達式。
- 向通道發(fā)送元素值或從通道那里接收元素值的接收表達式。
我們把以上這些表達式施加在某個值字面量上一般都會得到一個臨時結果。比如,對數(shù)組字面量和字典字面量的索引結果值,又比如,對數(shù)組字面量和切片字面量的切片結果值。它們都屬于臨時結果,都是不可尋址的。
一個需要特別注意的例外是,對切片字面量的索引結果值是可尋址的。因為不論怎樣,每個切片值都會持有一個底層數(shù)組,而這個底層數(shù)組中的每個元素值都是有一個確切的內存地址的。
那么對切片字面量的切片結果值為什么卻是不可尋址的?這是因為切片表達式總會返回一個新的切片值,而這個新的切片值在被賦給變量之前屬于臨時結果。
如果針對的是數(shù)組類型或切片類型的變量,那么索引或切片的結果值就都不屬于臨時結果了,是可尋址的。
這主要因為變量
的值本身就不是“臨時的”。對比而言,值字面量
在還沒有與任何變量(或者說任何標識符)綁定之前是沒有落腳點的,我們無法以任何方式引用到它們。這樣的值就是“臨時的”。
我們通過對字典類型的變量施加索引表達式,得到的結果值不屬于臨時結果,可是,這樣的值卻是不可尋址的。
原因是,字典中的每個鍵 - 元素對的存儲位置都可能會變化,而且這種變化外界是無法感知的。
字典中總會有若干個哈希桶用于均勻地儲存鍵 - 元素對。當滿足一定條件時,字典可能會改變哈希桶的數(shù)量,并適時地把其中的鍵 - 元素對搬運到對應的新的哈希桶中。在這種情況下,獲取字典中任何元素值的指針都是無意義的,也是不安全的。我們不知道什么時候那個元素值會被搬運到何處,也不知道原先的那個內存地址上還會被存放什么別的東西。所以,這樣的值就應該是不可尋址的。
第三個關鍵詞:不安全的。“不安全的”操作很可能會破壞程序的一致性,引發(fā)不可預知的錯誤,從而嚴重影響程序的功能和穩(wěn)定性。
函數(shù)在 Go 語言中是一等公民,所以我們可以把代表函數(shù)或方法的字面量或標識符賦給某個變量、傳給某個函數(shù)或者從某個函數(shù)傳出。但是,這樣的函數(shù)和方法都是不可尋址的。一個原因是函數(shù)就是代碼,是不可變的。
另一個原因是,拿到指向一段代碼的指針是不安全的。此外,對函數(shù)或方法的調用結果值也是不可尋址的,這是因為它們都屬于臨時結果。至于典型回答中最后列出的那幾種值,由于都是針對值字面量的某種表達式的結果值,所以都屬于臨時結果,都不可尋址。
- 不可變的值不可尋址。常量、基本類型的值字面量、字符串變量的值、函數(shù)以及方法的字面量都是如此。其實這樣規(guī)定也有安全性方面的考慮。
- 絕大多數(shù)被視為臨時結果的值都是不可尋址的。算術操作的結果值屬于臨時結果,針對值字面量的表達式結果值也屬于臨時結果。但有一個例外,對切片字面量的索引結果值,雖然也屬于臨時結果,但卻是可尋址的。
- 若拿到某值的指針可能會破壞程序的一致性,那么就是不安全的,該值就不可尋址。由于字典的內部機制,對字典的索引結果值的取址操作都是不安全的。另外,獲取由字面量或標識符代表的函數(shù)或方法的地址顯然也是不安全的。
不可尋址的值在使用上有哪些限制?
首當其沖的當然是無法使用取址操作符&
獲取它們的指針了。不過,對不可尋址的值施加取址操作都會使編譯器報錯,所以倒是不用太擔心,你只要記住我在前面講述的那幾條規(guī)律,并在編碼的時候提前注意一下就好了。
func New(name string) Dog { return Dog{name} }
我們再為它編寫一個函數(shù)New
。這個函數(shù)會接受一個名為name
的string
類型的參數(shù),并會用這個參數(shù)初始化一個Dog
類型的值,最后返回該值。我現(xiàn)在要問的是:如果我調用該函數(shù),并直接以鏈式的手法調用其結果值的指針方法SetName
,那么可以達到預期的效果嗎?
New("little pig").SetName("monster")
由于New
函數(shù)的調用結果的值是不可尋址的,所以無法對它進行取址操作。因此,上邊這行鏈式調用會讓編譯器報告兩個錯誤,一個是果,即:不能在New{"little pig"}
的結果值上調用指針方法。一個是因,即:不能取得New{"little pig"}
的地址。
除此之外,我們都知道,Go 語言中的++和–并不屬于操作符,而分別是自增語句和自減語句的重要組成部分。
雖然 Go 語言規(guī)范中的語法定義是,只要在++或–的左邊添加一個表達式,就可以組成一個自增語句或自減語句,但是,它還明確了一個很重要的限制,那就是這個表達式的結果值必須是可尋址的。這就使得針對值字面量的表達式幾乎都無法被用在這里。
不過這有一個例外,雖然對字典字面量和字典變量索引表達式的結果值都是不可尋址的,但是這樣的表達式卻可以被用在自增語句和自減語句中。
與之類似的規(guī)則還有兩個。一個是,在賦值語句中,賦值操作符左邊的表達式的結果值必須可尋址的,但是對字典的索引結果值也是可以的。
另一個是,在帶有range子句的for語句中,在range關鍵字左邊的表達式的結果值也都必須是可尋址的,不過對字典的索引結果值同樣可以被用在這里。
怎樣通過unsafe.Pointer操縱可尋址的值?
unsafe.Pointer
是像 * Dog
類型的值這樣的指針值和uintptr
值之間的橋梁,那么我們怎樣利用unsafe.Pointer
的中轉和uintptr
的底層操作來操縱像dog
這樣的值呢?
首先說明,這是一項黑科技。它可以繞過 Go 語言的編譯器和其他工具的重重檢查,并達到潛入內存修改數(shù)據(jù)的目的。這并不是一種正常的編程手段,使用它會很危險,很有可能造成安全隱患。
我們總是應該優(yōu)先使用常規(guī)代碼包中提供的 API 去編寫程序,當然也可以把像reflect
以及go/ast
這樣的代碼包作為備選項。作為上層應用的開發(fā)者,請謹慎地使用unsafe
包中的任何程序實體。
dog := Dog{"little pig"} dogP := &dog dogPtr := uintptr(unsafe.Pointer(dogP))
我先聲明了一個Dog類型的變量dog,然后用取址操作符&
,取出了它的指針值,并把它賦給了變量dogP。
最后,我使用了兩個類型轉換,先把dogP
轉換成了一個unsafe.Pointer
類型的值,然后緊接著又把后者轉換成了一個uintptr
的值,并把它賦給了變量dogPtr
。這背后隱藏著一些轉換規(guī)則,如下:
- 一個指針值(比如
* Dog
類型的值)可以被轉換為一個unsafe.Pointer
類型的值,反之
亦然。 - 一個
uintptr
類型的值也可以被轉換為一個unsafe.Pointer
類型的值,反之亦然。 - 一個指針值無法被直接轉換成一個
uintptr
類型的值,反過來也是如此。
所以,對于指針值和uintptr
類型值之間的轉換,必須使用unsafe.Pointer
類型的值作為中轉。那么,我們把指針值轉換成uintptr
類型的值有什么意義嗎?
namePtr := dogPtr + unsafe.Offsetof(dogP.name) nameP := (*string)(unsafe.Pointer(namePtr))
這里需要與unsafe.Offsetof
函數(shù)搭配使用才能看出端倪。unsafe.Offsetof
函數(shù)用于獲取兩個值在內存中的起始存儲地址之間的偏移量,以字節(jié)為單位。
這兩個值一個是某個字段的值,另一個是該字段值所屬的那個結構體值。我們在調用這個函數(shù)的時候,需要把針對字段的選擇表達式傳給它,比如dogP.name
。
有了這個偏移量,又有了結構體值在內存中的起始存儲地址(這里由dogPtr
變量代表),把它們相加我們就可以得到dogP
的name
字段值的起始存儲地址了。這個地址由變量namePtr
代表。
此后,我們可以再通過兩次類型轉換把namePtr
的值轉換成一個* string
類型的值,這樣就得到了指向dogP
的name
字段值的指針值。
你可能會問,我直接用取址表達式&(dogP.name)不就能拿到這個指針值了嗎?干嘛繞這么大一圈呢?你可以想象一下,如果我們根本就不知道這個結構體類型是什么,也拿不到dogP這個變量,那么還能去訪問它的name字段嗎?
答案是,只要有namePtr
就可以。它就是一個無符號整數(shù),但同時也是一個指向了程序內部數(shù)據(jù)的內存地址。它可能會給我們帶來一些好處,比如可以直接修改埋藏得很深的內部數(shù)據(jù)。
但是,一旦我們有意或無意地把這個內存地址泄露出去,那么其他人就能夠肆意地改動dogP.name
的值,以及周圍的內存地址上存儲的任何數(shù)據(jù)了。
以上就是關于golang指針的有限操作詳解的詳細內容,更多關于golang指針操作的資料請關注腳本之家其它相關文章!
相關文章
Go錯誤處理之panic函數(shù)和recover函數(shù)使用及捕獲異常方法
這篇文章主要介紹了Go錯誤處理之panic函數(shù)使用及捕獲,本篇探討了如何使用 panic 和 recover 來處理 Go 語言中的異常,需要的朋友可以參考下2023-03-03viper配置框架的介紹支持zookeeper的讀取和監(jiān)聽
這篇文章主要介紹了viper配置框架的介紹支持zookeeper的讀取和監(jiān)聽,本文通過實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-05-05關于Golang中range指針數(shù)據(jù)的坑詳解
這篇文章主要給大家介紹了關于Golang中range指針數(shù)據(jù)的坑的相關資料,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2019-02-02