Golang的寻址与不可寻址

1,740 阅读5分钟

在学习结构体的时候,无意发现一段代码的区别,最终才有了这篇文章,我们先看看这段代码:

样例代码

package main

import "fmt"

func main() {
	fmt.Println(person{}.foo())      //这是值类型
	fmt.Println(person{}.fooPoint()) //cannot call pointer method on person literal

	fmt.Println((&person{}).foo())      //这是值类型
	fmt.Println((&person{}).fooPoint()) //这是指针类型
}

type person struct {
}

func (p person) foo() string {
	return "这是值类型"
}

func (p *person) fooPoint() string {
	return "这是指针类型"
}

代码很简单,定义一个person结构体,又定义了结构体的两个方法,一个是值接收者类型方法foo();一个是指针接收者类型方法fooPoint()。但是在main方法里面的四句打印语句,发现fmt.Println(person{}.fooPoint())这句是编译报错的:cannot call pointer method on person literal指针方法和值方法都可以在指针或非指针上被调用,在golang内部是会自动转换的,golang会自动帮忙取地址操作的,之前就一直有这么一句话的,为什么这里这句就会报错呢?

原因就是person{}是不可寻址的

我们先看改进的代码:

package main

import "fmt"

func main() {
	p := person{}
	//fmt.Println(p.foo())      //这是值类型
	fmt.Println(p.fooPoint()) //这是指针类型

	//po := &person{}
	//fmt.Println(po.foo())      //这是值类型
	//fmt.Println(po.fooPoint()) //这是指针类型
}

type person struct {
}

func (p person) foo() string {
	return "这是值类型"
}

func (p *person) fooPoint() string {
	return "这是指针类型"
}

我们就是定义一个p,然后通过p.fooPoint()来调用,就可以了。说明p这个变量是可以寻址的。

什么叫可寻址

可直接使用 & 操作符取地址的对象,就是可寻址的。比如下面这个例子

name := "yif"
fmt.Println(&name) //0xc00003c1f0 【说明 name 这个变量是可寻址的】

但不能说 "yif" 这个字符串是可寻址的。"yif" 是字符串,字符串都是不可变的,是不可寻址的。

 哪些是可以寻址的

变量:&x

name := "yif"
fmt.Println(&name) //0xc00003c1f0

指针:&*x

package main

import (
	"fmt"
	"unsafe"
)

func main() {
	fmt.Println(unsafe.Pointer(&person{"yif"})) //0xc00003c1f0
}

type person struct {
	name string
}

数组元素索引: &a[0]

s := [3]int{1,2,3}
fmt.Printf("%p\n",&s[0]) //0xc00000e3c0

切片

fmt.Println([]int{1, 2, 3}[1:]) //[2 3]

切片元素索引:&s[0]

s := []int{1, 2, 3}
fmt.Println(&s[0]) //0xc00000e3c0

组合字面量: &struct{X type}{value}

所有的组合字面量都是不可寻址的,就像下面这样子

func main() {
	fmt.Println(&getP()) // cannot take the address of getP()
}

type person struct {
	name string
}

func getP() person {
	return person{name: "yif"}
}

注意上面写法与这个写法的区别,下面这个写法代表不同意思,其中的 & 并不是取地址的操作,而代表实例化一个结构体的指针

func main() {
	fmt.Println(&person{name: "yif"}) // &{yif}
}

type person struct {
	name string
}

func getP() person {
	return person{name: "yif"}
} 

虽然组合字面量是不可寻址的,但却可以对组合字面量的字段属性进行寻址(直接访问)

func main() {
	fmt.Println(getP().name) // yif
}

type person struct {
	name string
}

func getP() person {
	return person{name: "yif"}
}

哪些是不可以寻址

常量

const PI  = 3.14
func main() {
	fmt.Println(&PI) //  cannot take the address of PI
} 

字符串

func main() {
	fmt.Println(&getStr()) //  cannot take the address of getStr()
	//fmt.Printf("%p\n",&getStr()) //  cannot take the address of getStr()
}


func getStr() string {
	return "yif"
}

函数或方法

func main() {
	fmt.Println(&getStr) //  cannot take the address of getStr
	//fmt.Printf("%p\n",&getStr) //  cannot take the address of getStr
}


func getStr() string {
	return "yif"
}

基本类型字面量

字面量分:基本类型字面量复合型字面量

基本类型字面量,是一个值的文本表示,都是不应该也是不可以被寻址的。

func getInt() int {
	return 1024
}

func main() {
	fmt.Println(&getInt()) // cannot take the address of getInt()
} 

map 中的元素

字典比较特殊,可以从两个角度来反向推导,假设字典的元素是可寻址的,会出现 什么问题?

  1. 如果字典的元素不存在,则返回零值,而零值是不可变对象,如果能寻址问题就大了。

  2. 而如果字典的元素存在,考虑到 Go 中 map 实现中元素的地址是变化的,这意味着寻址的结果也是无意义的。

基于这两点,Map 中的元素不可寻址,符合常理。

func main() {
	m := map[string]int{
		"yif": 20,
	}

	//fmt.Println(&m["yif"]) // cannot take the address of m["yif"]
	fmt.Printf("%p\n",&m["yif"]) // cannot take the address of m["yif"]
}

搞懂了这点,你应该能够理解下面这段代码为什么会报错啦,map里面不能修改结构体的属性。

func main() {
	m := map[int]person{
		1: {name: "yif"},
		2: {name: "tom"},
	}

	m[1].name = "James" //cannot assign to struct field m[1].name in map
}

type person struct {
	name string
}

如果需要改结构体里面的属性,可以改为如下:

func main() {
	m := map[int]*person{ //结构体是指针
		1: &person{name: "yif"},
		2: &person{name: "tom"},
	}

	fmt.Println(m[1].name) //yif
	m[1].name = "James"
	fmt.Println(m[1].name) //James
}

type person struct {
	name string
}

数组字面量

数组字面量是不可寻址的,当你对数组字面量进行切片操作,其实就是寻找内部元素的地址,下面这段代码是会报错的。

func main() {
	fmt.Println([3]int{1, 2, 3}[1:]) //invalid operation [3]int literal[1:] (slice of unaddressable value)
}

其实基本上要达到寻址,就通过使用一个变量承接一下,比如:

func main() {
        arr := [3]int{1, 2, 3}
	fmt.Println(arr[1:]) //[2,3]
}

还有之前数组那个例子(第二种写法就不对):

s := [3]int{1,2,3}
fmt.Printf("%p\n",&s[0]) //0xc00000e3c0

fmt.Printf("%p\n",&[3]int{1,2,3}[0]) //cannot take the address of [3]int literal[0]

至于字面量的内容,去看这篇Golang的字面量

参考文章:

segmentfault.com/a/119000002…
Go 中的可寻址和不可寻址怎么理解?