域名怎么选才正确,网站seo优化全程记录思维导图,一对一直播系统开发,网站建设qq群传统意义上来说#xff0c;指针是一个指向某个确切的内存地址的值。这个内存地址可以是任何数据或代码的起始地址。在Go语言中有几种东西可以代表指针。其中最贴切传统意义的当属uintptr类型的了。该类型实际上是一个数值类型#xff0c;也是Go语言内建的数据类型…传统意义上来说指针是一个指向某个确切的内存地址的值。这个内存地址可以是任何数据或代码的起始地址。在Go语言中有几种东西可以代表指针。其中最贴切传统意义的当属uintptr类型的了。该类型实际上是一个数值类型也是Go语言内建的数据类型之一。
根据当前计算机的计算架构的不同它可以存储32位或64位的无符号整数可以代表任何指针的位bit模式也就是原始的内存地址。
Go语言标准库中的unsafe包unsafe包中有一个类型叫做Pointer,也代表任何指针的位bit模式也就是原始的内存地址。
unsafe.Pointer可以表示任何指向可寻址的值的指针同时它也是前面提到的指针值和uintptr值之间的桥梁。通过它我们可以在这两种值之上进行双向的转换。这里有一个很关键的词——可寻址的addressable。
在我们继续说unsafe.Pointer之前需要先搞清楚这个词的确切含义。
Go语言中的哪些值是不可寻址的
常量的值。基本类型值的字面量。算术操作的结果值。对各种字面量的索引表达式和切片表达式的结果值。不过有一个例外对切片字面量的索引结果值却是可寻址的。对字符串变量的索引表达式和切片表达式的结果值。对字典变量的索引表达式的结果值。函数字面量和方法字面量以及对它们的调用表达式的结果值。结构体字面量的字段值也就是对结构体字面量的选择表达式的结果值。类型转换表达式的结果值。类型断言表达式的结果值。接收表达式的结果值。
// 示例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]) // 对数组字面量的索引结果值不可寻址。//_ ([3]int{1, 2, 3}[0:2]) // 对数组字面量的切片结果值不可寻址。_ ([]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//}) // 字面量代表的函数不可寻址。//_ (fmt.Sprintf) // 标识符代表的函数不可寻址。//_ (fmt.Sprintln(abc)) // 对函数的调用结果值不可寻址。dog : Dog{little pig}_ dog//_ (dog.Name) // 标识符代表的函数不可寻址。//_ (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) // 接收表达式的结果值不可寻址。常量的值总是会被存储到一个确切的内存区域中并且这种值肯定是不可变的。基本类型值的字面量也是一样其实它们本就可以被视为常量只不过没有任何标识符可以代表它们罢了。
第一个关键词不可变的。由于 Go 语言中的字符串值也是不可变的所以对于一个字符串类型的变量来说基于它的索引或切片的结果值也都是不可寻址的因为即使拿到了这种值的内存地址也改变不了什么。
算术操作的结果值属于一种临时结果。在我们把这种结果值赋给任何变量或常量之前即使能拿到它的内存地址也是没有任何意义的。
第二个关键词临时结果。这个关键词能被用来解释很多现象。我们可以把各种对值字面量施加的表达式的求值结果都看做是临时结果。
我们都知道Go 语言中的表达式有很多种其中常用的包括以下几种。
用于获得某个元素的索引表达式。用于获得某个切片片段的切片表达式。用于访问某个字段的选择表达式。用于调用某个函数或方法的调用表达式。用于转换值的类型的类型转换表达式。用于判断值的类型的类型断言表达式。向通道发送元素值或从通道那里接收元素值的接收表达式。
我们把以上这些表达式施加在某个值字面量上一般都会得到一个临时结果。比如对数组字面量和字典字面量的索引结果值又比如对数组字面量和切片字面量的切片结果值。它们都属于临时结果都是不可寻址的。
一个需要特别注意的例外是对切片字面量的索引结果值是可寻址的。因为不论怎样每个切片值都会持有一个底层数组而这个底层数组中的每个元素值都是有一个确切的内存地址的。
那么对切片字面量的切片结果值为什么却是不可寻址的这是因为切片表达式总会返回一个新的切片值而这个新的切片值在被赋给变量之前属于临时结果。
如果针对的是数组类型或切片类型的变量那么索引或切片的结果值就都不属于临时结果了是可寻址的。
这主要因为变量的值本身就不是“临时的”。对比而言值字面量在还没有与任何变量或者说任何标识符绑定之前是没有落脚点的我们无法以任何方式引用到它们。这样的值就是“临时的”。
我们通过对字典类型的变量施加索引表达式得到的结果值不属于临时结果可是这样的值却是不可寻址的。原因是字典中的每个键 - 元素对的存储位置都可能会变化而且这种变化外界是无法感知的。
字典中总会有若干个哈希桶用于均匀地储存键 - 元素对。当满足一定条件时字典可能会改变哈希桶的数量并适时地把其中的键 - 元素对搬运到对应的新的哈希桶中。在这种情况下获取字典中任何元素值的指针都是无意义的也是不安全的。我们不知道什么时候那个元素值会被搬运到何处也不知道原先的那个内存地址上还会被存放什么别的东 西。所以这样的值就应该是不可寻址的。
第三个关键词不安全的。“不安全的”操作很可能会破坏程序的一致性引发不可预知的错误从而严重影响程序的功能和稳定性。
函数在 Go 语言中是一等公民所以我们可以把代表函数或方法的字面量或标识符赋给某个变量、传给某个函数或者从某个函数传出。但是这样的函数和方法都是不可寻址的。一个原因是函数就是代码是不可变的。
另一个原因是拿到指向一段代码的指针是不安全的。此外对函数或方法的调用结果值也是不可寻址的这是因为它们都属于临时结果。至于典型回答中最后列出的那几种值由于都是针对值字面量的某种表达式的结果值所以都属于临时结果都不可寻址。
不可变的值不可寻址。常量、基本类型的值字面量、字符串变量的值、函数以及方法的字面量都是如此。其实这样规定也有安全性方面的考虑。绝大多数被视为临时结果的值都是不可寻址的。算术操作的结果值属于临时结果针对值字面量的表达式结果值也属于临时结果。但有一个例外对切片字面量的索引结果值,虽然也属于临时结果但却是可寻址的。若拿到某值的指针可能会破坏程序的一致性那么就是不安全的该值就不可寻址。由于字典的内部机制对字典的索引结果值的取址操作都是不安全的。另外获取由字面量或标识符代表的函数或方法的地址显然也是不安全的。
不可寻址的值在使用上有哪些限制
首当其冲的当然是无法使用取址操作符获取它们的指针了。不过对不可寻址的值施加取址操作都会使编译器报错所以倒是不用太担心你只要记住我在前面讲述的那几条规律并在编码的时候提前注意一下就好了。
func New(name string) Dog {return Dog{name}
}我们再为它编写一个函数New。这个函数会接受一个名为name的string类型的参数并会用这个参数初始化一个Dog类型的值最后返回该值。我现在要问的是如果我调用该函数并直接以链式的手法调用其结果值的指针方法SetName那么可以达到预期的效果吗 New(little pig).SetName(monster)由于New函数的调用结果的值是不可寻址的所以无法对它进行取址操作。因此上边这行链式调用会让编译器报告两个错误一个是果即不能在New{little pig}的结果值上调用指针方法。一个是因即不能取得New{little pig}的地址。
除此之外我们都知道Go 语言中的和–并不属于操作符而分别是自增语句和自减语句的重要组成部分。 虽然 Go 语言规范中的语法定义是只要在或–的左边添加一个表达式就可以组成一个自增语句或自减语句但是它还明确了一个很重要的限制那就是这个表达式的结果值必须是可寻址的。这就使得针对值字面量的表达式几乎都无法被用在这里。
不过这有一个例外虽然对字典字面量和字典变量索引表达式的结果值都是不可寻址的但是这样的表达式却可以被用在自增语句和自减语句中。
与之类似的规则还有两个。一个是在赋值语句中赋值操作符左边的表达式的结果值必须可寻址的但是对字典的索引结果值也是可以的。
另一个是在带有range子句的for语句中在range关键字左边的表达式的结果值也都必须是可寻址的不过对字典的索引结果值同样可以被用在这里。
怎样通过unsafe.Pointer操纵可寻址的值
unsafe.Pointer是像 * Dog 类型的值这样的指针值和uintptr值之间的桥梁那么我们怎样利用unsafe.Pointer的中转和uintptr的底层操作来操纵像dog这样的值呢
首先说明这是一项黑科技。它可以绕过 Go 语言的编译器和其他工具的重重检查并达到潜入内存修改数据的目的。这并不是一种正常的编程手段使用它会很危险很有可能造成安全隐患。
我们总是应该优先使用常规代码包中提供的 API 去编写程序当然也可以把像reflect以及go/ast这样的代码包作为备选项。作为上层应用的开发者请谨慎地使用unsafe包中的任何程序实体。
dog : Dog{little pig}
dogP : dog
dogPtr : uintptr(unsafe.Pointer(dogP))我先声明了一个Dog类型的变量dog然后用取址操作符取出了它的指针值并把它赋给了变量dogP。
最后我使用了两个类型转换先把dogP转换成了一个unsafe.Pointer类型的值然后紧接着又把后者转换成了一个uintptr的值并把它赋给了变量dogPtr。这背后隐藏着一些转换规则如下
一个指针值比如* Dog类型的值可以被转换为一个unsafe.Pointer类型的值反之 亦然。一个uintptr类型的值也可以被转换为一个unsafe.Pointer类型的值反之亦然。一个指针值无法被直接转换成一个uintptr类型的值反过来也是如此。
所以对于指针值和uintptr类型值之间的转换必须使用unsafe.Pointer类型的值作为中转。那么我们把指针值转换成uintptr类型的值有什么意义吗
namePtr : dogPtr unsafe.Offsetof(dogP.name)
nameP : (*string)(unsafe.Pointer(namePtr))这里需要与unsafe.Offsetof函数搭配使用才能看出端倪。unsafe.Offsetof函数用于获取两个值在内存中的起始存储地址之间的偏移量以字节为单位。
这两个值一个是某个字段的值另一个是该字段值所属的那个结构体值。我们在调用这个函数的时候需要把针对字段的选择表达式传给它比如dogP.name。
有了这个偏移量又有了结构体值在内存中的起始存储地址这里由dogPtr变量代表把它们相加我们就可以得到dogP的name字段值的起始存储地址了。这个地址由变量namePtr代表。
此后我们可以再通过两次类型转换把namePtr的值转换成一个* string类型的值这样就得到了指向dogP的name字段值的指针值。
你可能会问我直接用取址表达式(dogP.name)不就能拿到这个指针值了吗干嘛绕这么大一圈呢你可以想象一下如果我们根本就不知道这个结构体类型是什么也拿不到dogP这个变量那么还能去访问它的name字段吗
答案是只要有namePtr就可以。它就是一个无符号整数但同时也是一个指向了程序内部数据的内存地址。它可能会给我们带来一些好处比如可以直接修改埋藏得很深的内部数据。
但是一旦我们有意或无意地把这个内存地址泄露出去那么其他人就能够肆意地改动dogP.name的值以及周围的内存地址上存储的任何数据了。
文章学习自郝林老师的《Go语言36讲》