溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

Go36-15-指針

發(fā)布時(shí)間:2020-07-04 09:24:32 來(lái)源:網(wǎng)絡(luò) 閱讀:456 作者:騎士救兵 欄目:編程語(yǔ)言

指針

之前已經(jīng)用到過(guò)很多次指針了,不過(guò)大多數(shù)時(shí)候是指指針類(lèi)型及其對(duì)應(yīng)的指針值。這里要講更為深入的內(nèi)容。

其他指針

從傳統(tǒng)意義上說(shuō),指針是一個(gè)指向某個(gè)確切的內(nèi)存地址的值。這個(gè)內(nèi)存地址可以是任何數(shù)據(jù)或代碼的起始地址,比如,某個(gè)變量、某個(gè)字段或某個(gè)函數(shù)。

uintptr
在Go語(yǔ)言中還有其他幾樣?xùn)|西可以代表“指針”。其中最貼近傳統(tǒng)意義的當(dāng)屬u(mài)intptr類(lèi)型了。該類(lèi)型實(shí)際上是一個(gè)數(shù)值類(lèi)型,也是Go語(yǔ)言?xún)?nèi)建的數(shù)據(jù)類(lèi)型之一。根據(jù)當(dāng)前計(jì)算機(jī)的計(jì)算架構(gòu)的不同,它可以存儲(chǔ)32位或64位的無(wú)符號(hào)整數(shù),可以代表任何指針的位(bit)模式,也就是原始的內(nèi)存地址。

unsafe.Pointer
在Go語(yǔ)言標(biāo)準(zhǔn)庫(kù)中的unsafe包。unsafe包中有一個(gè)類(lèi)型叫做Pointer,也代表了“指針”。unsafe.Pointer可以表示任何指向可尋址的值的指針,同時(shí)它也是前面提到的指針值和uintptr值之間的橋梁。也就是說(shuō),通過(guò)它,我們可以在這兩種值之上進(jìn)行雙向的轉(zhuǎn)換。這里有一個(gè)很關(guān)鍵的詞——可尋址的(addressable)。在我們繼續(xù)說(shuō)unsafe.Pointer之前,需要先要搞清楚這個(gè)詞的確切含義。

不可尋址的值

一下的值都是不可尋址的:

  • 常量的值
  • 基本類(lèi)型值的字面量
  • 算數(shù)操作的結(jié)果值
  • 對(duì)各種字面量的索引表達(dá)式和切片表達(dá)式的結(jié)果值。例外,切片字面量的索引結(jié)果值是可尋址的
  • 對(duì)字符串變量的索引表達(dá)式和切片表達(dá)式的結(jié)果值
  • 對(duì)字典變量的索引表達(dá)式的結(jié)果值
  • 函數(shù)字面量和方法字面量,以及對(duì)他們的調(diào)用表達(dá)式的結(jié)果值
  • 結(jié)構(gòu)體字面量的字段值,也就是對(duì)結(jié)構(gòu)體字面量的選擇表達(dá)式的結(jié)果值
  • 類(lèi)型轉(zhuǎn)換表達(dá)式的結(jié)果值
  • 類(lèi)型斷言表達(dá)式的結(jié)果值
  • 接收表達(dá)式的結(jié)果值

上面一堆術(shù)語(yǔ),看看在代碼里具體指的是哪些類(lèi)型:

package main

type Named interface {
    // Name 用于獲取名字。
    Name() string
}

type Dog struct {
    name string
}

func (dog *Dog) SetName(name string) {
    dog.name = name
}

func (dog Dog) Name() string {
    return dog.name
}

func main() {
    // 示例1。
    const num = 123
    //_ = &num // 常量不可尋址。
    //_ = &(123) // 基本類(lèi)型值的字面量不可尋址。

    var str = "abc"
    _ = str
    //_ = &(str[0]) // 對(duì)字符串變量的索引結(jié)果值不可尋址。
    //_ = &(str[0:2]) // 對(duì)字符串變量的切片結(jié)果值不可尋址。
    str2 := str[0]
    _ = &str2 // 但這樣的尋址就是合法的。

    //_ = &(123 + 456) // 算術(shù)操作的結(jié)果值不可尋址。
    num2 := 456
    _ = num2
    //_ = &(num + num2) // 算術(shù)操作的結(jié)果值不可尋址。

    //_ = &([3]int{1, 2, 3}[0]) // 對(duì)數(shù)組字面量的索引結(jié)果值不可尋址。
    //_ = &([3]int{1, 2, 3}[0:2]) // 對(duì)數(shù)組字面量的切片結(jié)果值不可尋址。
    _ = &([]int{1, 2, 3}[0]) // 對(duì)切片字面量的索引結(jié)果值卻是可尋址的。
    //_ = &([]int{1, 2, 3}[0:2]) // 對(duì)切片字面量的切片結(jié)果值不可尋址。
    //_ = &(map[int]string{1: "a"}[0]) // 對(duì)字典字面量的索引結(jié)果值不可尋址。

    var map1 = map[int]string{1: "a", 2: "b", 3: "c"}
    _ = map1
    //_ = &(map1[2]) // 對(duì)字典變量的索引結(jié)果值不可尋址。

    //_ = &(func(x, y int) int {
    //  return x + y
    //}) // 字面量代表的函數(shù)不可尋址。
    //_ = &(fmt.Sprintf) // 標(biāo)識(shí)符代表的函數(shù)不可尋址。
    //_ = &(fmt.Sprintln("abc")) // 對(duì)函數(shù)的調(diào)用結(jié)果值不可尋址。

    dog := Dog{"little pig"}
    _ = dog
    //_ = &(dog.Name) // 標(biāo)識(shí)符代表的函數(shù)不可尋址。
    //_ = &(dog.Name()) // 對(duì)方法的調(diào)用結(jié)果值不可尋址。

    //_ = &(Dog{"little pig"}.name) // 結(jié)構(gòu)體字面量的字段不可尋址。

    //_ = &(interface{}(dog)) // 類(lèi)型轉(zhuǎn)換表達(dá)式的結(jié)果值不可尋址。
    dogI := interface{}(dog)
    _ = dogI
    //_ = &(dogI.(Named)) // 類(lèi)型斷言表達(dá)式的結(jié)果值不可尋址。
    named := dogI.(Named)
    _ = named
    //_ = &(named.(Dog)) // 類(lèi)型斷言表達(dá)式的結(jié)果值不可尋址。

    var chan1 = make(chan int, 1)
    chan1 <- 1
    //_ = &(<-chan1) // 接收表達(dá)式的結(jié)果值不可尋址。
}

總結(jié)一個(gè)不可尋址的值的特點(diǎn):

  1. 不可變的值不可尋址。常量、基本類(lèi)型的值字面量、字符串變量的值、函數(shù)以及方法的字面量都是如此。其實(shí)這樣規(guī)定也有安全性方面的考慮。
  2. 絕大多數(shù)被視為臨時(shí)結(jié)果的值都是不可尋址的。算術(shù)操作的結(jié)果值屬于臨時(shí)結(jié)果,針對(duì)值字面量的表達(dá)式結(jié)果值也屬于臨時(shí)結(jié)果。但有一個(gè)例外,對(duì)切片字面量的索引結(jié)果值雖然也屬于臨時(shí)結(jié)果,但卻是可尋址的。
  3. 若拿到某值的指針可能會(huì)破壞程序的一致性,那么就是不安全的,該值就不可尋址。由于字典的內(nèi)部機(jī)制,對(duì)字典的索引結(jié)果值的取址操作都是不安全的。另外,獲取由字面量或標(biāo)識(shí)符代表的函數(shù)或方法的地址顯然也是不安全的。

最后,如果把臨時(shí)結(jié)果賦值給一個(gè)變量,那么它就是可尋址的了。

不可尋址的值的限制
無(wú)法使用取址操作符&獲取他們的指針。如果嘗試取址會(huì)是編譯器報(bào)錯(cuò),所以不用太擔(dān)心。這里再看個(gè)小問(wèn)題:

package main

import "fmt"

type Dog struct {
    name string
}

func (d *Dog) SetName (name string) {
    d.name = name
}

func New(name string) Dog {
    return Dog{name}
}

func main() {
    obj := New("Snoopy")
    obj.SetName("Goofy")
    fmt.Println(obj.name)
    // New("Snoopy").SetName("Wishbone")  //
}

這里寫(xiě)了一個(gè)New函數(shù),用于獲取Dog的結(jié)構(gòu)體。返回的是結(jié)構(gòu)體的值類(lèi)型。還有一個(gè)指針?lè)椒?,這里直接對(duì)值類(lèi)型調(diào)用指針?lè)椒ㄊ菦](méi)有問(wèn)題的。因?yàn)闀?huì)被自動(dòng)轉(zhuǎn)譯成(&dog).SetName("Goofy")。但是New函數(shù)的調(diào)用結(jié)果值是不可尋址的,所以最后一行嘗試直接以鏈?zhǔn)降姆椒ㄕ{(diào)用就會(huì)有編譯問(wèn)題。這個(gè)不可取址的情況應(yīng)該是屬于臨時(shí)結(jié)果,所以把結(jié)果賦值給一個(gè)變量,再調(diào)用指針?lè)椒ㄊ菦](méi)有問(wèn)題的。

自增++和自減--
另外,在Go語(yǔ)言中++和--不屬于操作符,而是自增語(yǔ)句或自減語(yǔ)句的組成部分。只要在++或--的左邊添加一個(gè)表達(dá)式,就組成了一個(gè)自增語(yǔ)句或自減語(yǔ)句,但是表達(dá)式的結(jié)果值必須是可尋址的。比如值字面的表達(dá)式就是無(wú)法自增的1++。
這里也有例外,字典字面量和字典變量索引表達(dá)式的結(jié)果值都是不可尋址的,但是可以自增、自減。
類(lèi)似的規(guī)則還有兩個(gè):

  1. 賦值語(yǔ)句,賦值操作符左邊的表達(dá)式的結(jié)果值必須是可尋址的。但是對(duì)字典的索引結(jié)果值也是賦值
  2. 帶有range子句的for語(yǔ)句中,在range關(guān)鍵字左邊的表達(dá)式的結(jié)果值也必須是可尋址的。還是對(duì)字典的索引結(jié)果值例外。

unsafe.Pointer黑科技

下面講的方法,可以繞過(guò)Go語(yǔ)言的編譯器和其他工具的重重檢查,并達(dá)到潛入內(nèi)存修改數(shù)據(jù)的目的。這不是一種正常的手段,使用它會(huì)很危險(xiǎn),還很可能造成安全隱患。我們總是應(yīng)該優(yōu)先使用常規(guī)代碼包中提供的API去編寫(xiě)程序,當(dāng)然也可以把像reflect以及go/ast這樣的代碼包作為備選項(xiàng)。
指針值、unsafe.Pointer、uintptr有如下的轉(zhuǎn)換規(guī)則:

  1. 指針值和unsafe.Pointer可以互相轉(zhuǎn)換
  2. uintptr和unsafe.Pointer也可以互相轉(zhuǎn)換
  3. 指針值和uintptr無(wú)法直接互相轉(zhuǎn)換

所以說(shuō)unsafe.Pointer是指針值和uintptr值之間的橋梁。到這一步,我們現(xiàn)在已經(jīng)可以獲取到變量的uintptr類(lèi)型的值了:

s := student{}
sP := &s
sPtr := uintptr(unsafe.Pointer(sP))

unsafe.Offsetof 的使用
unsafe.Offsetof函數(shù)返回變量(struct類(lèi)型)指定屬性的偏移量,以字節(jié)為單位。如下使用就可以獲取到結(jié)構(gòu)體的屬性相對(duì)于結(jié)構(gòu)體的偏移量了:

func main() {
    type student struct {
        name string
        age int
    }
    s1 := student{}
    p1 := unsafe.Offsetof((&s1).name)  // 結(jié)構(gòu)體的第一個(gè)變量,偏移量是0
    p2 := unsafe.Offsetof((&s1).age)  // 這里就會(huì)有偏移量了
    fmt.Println(p1, p2)
}

搭配使用獲取屬性的地址
簡(jiǎn)單的把結(jié)構(gòu)體的地址和屬性的偏移量相加,就能獲得屬性的地址了。獲取到了屬性的地址后,如果再對(duì)這個(gè)地址做兩次地址轉(zhuǎn)換,就變回屬性的指針值了:

package main

import (
    "unsafe"
    "fmt"
)

func main() {
    type student struct {
        name string
        age int
    }
    s1 := student{"Adam", 18}
    s1P := &s1
    s1Ptr := uintptr(unsafe.Pointer(s1P))  // 結(jié)構(gòu)體的地址
    fmt.Println(s1Ptr)
    namePtr := s1Ptr + unsafe.Offsetof(s1P.name)  // name屬性的地址
    agePtr := s1Ptr + unsafe.Offsetof(s1P.age)  // age屬性的地址
    fmt.Println(namePtr, agePtr)
    nameP := (*string)(unsafe.Pointer(namePtr))  // 獲取到屬性的指針
    ageP := (*int)(unsafe.Pointer(agePtr))
    fmt.Println(*nameP, *ageP)  // 取值獲取到屬性指針的值
}

上面的方法,饒了一大圈就是為了獲取到結(jié)構(gòu)體里屬性的地址。有了地址就可以對(duì)操作,也就可以直接修改埋藏的很深的內(nèi)部數(shù)據(jù)了。比如可以直接結(jié)果別的包里的結(jié)構(gòu)體內(nèi)的不可導(dǎo)出的屬性值。

修改結(jié)構(gòu)體不可導(dǎo)出的屬性值
知識(shí)點(diǎn)都在上面了,這里直接試著修改別的包的結(jié)構(gòu)體內(nèi)的不可導(dǎo)出的屬性的值:

// article15/example06/model/s.go
package model

// 結(jié)構(gòu)體屬性全小寫(xiě)
type Student struct {
    name string
    age int
}

// article15/example06/main.go
package main

import (
    "Go36/article15/example06/model"
    "fmt"
    "unsafe"
)

func main() {
    s1 := model.Student{}
    s1P := &s1
    s1Ptr := uintptr(unsafe.Pointer(s1P))
    namePtr := s1Ptr + 0
    agePtr := s1Ptr + 16
    nameP := (*string)(unsafe.Pointer(namePtr))
    ageP := (*int)(unsafe.Pointer(agePtr))
    *nameP = "Adam"
    *ageP = 22
    fmt.Println(s1)
}

這里unsafe.Pointer類(lèi)型和uintptr類(lèi)型所代表指針更貼近于底層和內(nèi)存,理論上可以利用它們?nèi)ピL問(wèn)或修改一些內(nèi)部數(shù)據(jù)。但是這么用會(huì)帶來(lái)安全隱患,在很多時(shí)候,使用它們操縱數(shù)據(jù)是弊大于利的??傊谰托辛?,別這么用。

向AI問(wèn)一下細(xì)節(jié)
推薦閱讀:
  1. golang 指針
  2. 指針150206204

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

go
AI