Go语言的结构体、方法、指针

目录

【定义新数据类型】

【结构体】

定义结构体

结构体变量的声明和初始化

结构体的内存表示

【方法】

receiver 参数

receiver参数的约束

方法的深入理解

goroutine中方法的使用

receiver 参数类型如何选择?T还是*T?

方法集合

【指针】

uintptr和unsafe.Pointer


【定义新数据类型】

之前说过Go语言中可以基于一个已有类型定义一个新类型,比如下面这样:

type MyInt int //基于底层类型int定义一个新类型MyInt
type YourInt MyInt //基于上面新定义的MyInt再定义一个新类型YourInt

如果一个新类型是基于某个 Go 原生类型定义的,那么这个类型就叫做新类型的底层类型。上面的int是MyInt的底层类型,也是YourInt的底层类型。

Go 语言中底层类型用来判断两个类型本质上是否相同,本质相同的两个类型的变量可以通过显式转型进行相互赋值,相反的如果本质上是不同的两个类型,它们的变量间连显式转型都不可能,更不要说相互赋值了。

package main

import "fmt"

type MyInt int
type YourInt MyInt
type MyString string

func main() {
	var n1 MyInt
	var n2 YourInt = 5
	n1 = MyInt(n2) // ok
	fmt.Println(n1)
	var s MyString = "hello"
	//n1 = MyInt(s) // 错误:cannot convert s (variable of type MyString) to type MyInt
	fmt.Println(s)
}

也可以基于复合类型创建一个新的类型,并且可以把多个定义放在一起

type (
	MyInt     int
	YourInt   MyInt
	MyString  string
	mapIntStr map[int]string
	sliStr    []string
)

需要注意的是,type T = S 这种格式表示类型别名,两者没有任何区别。可以对比下面的代码:

package main

import "fmt"

type IntNew int    //基于int类型定义了一个新的类型:IntNew
type IntSelf = int //对int类型起了个别名:IntSelf

func main(){
    //定义新类型 和 类型别名 的区别
	var n3 IntNew
	var n4 IntSelf
	fmt.Printf("n3: %v, %T \n", n3, n3) //n3: 0, main.IntNew
	fmt.Printf("n4: %v, %T \n", n4, n4) //n4: 0, int
}

【结构体】

定义结构体

定义一个结构体使用 type 和 struct 关键词,一个结构体由若干个字段聚合而成,每个字段有自己的名字与类型,并且在一个结构体中,每个字段的名字应该都是唯一的。就像是PHP或Java中的类(class)一样。下面是定义结构体的方式:

// 定义一个结构体.首字母大写才能被其它包访问
type User struct {
	Name  string
	Age   int
	hobby []string
    _     string //空标识符,不能被外部包和结构体所在的包使用
}

空结构体类型具有内存零开销的特性,因此经常以空结构体为元素类建立 channel,这是目前能实现的内存占用最小的 Goroutine 间通信方式。

// 定义一个空结构体
type Empty struct{}

func StructDemo1() {
	var s1 User
	var s2 Empty
	fmt.Println("s1:", unsafe.Sizeof(s1)) // 64
	fmt.Println("s2:", unsafe.Sizeof(s2)) // 空结构体类型变量的内存占用为 0

	//基于空结构体类型内存零开销的特性,经常以空结构体为元素类建立 channel,是目前能实现的内存占用最小的 Goroutine 间通信方式。
	var c = make(chan Empty) //声明一个元素类型为Empty的channel
	c <- Empty{}             //向channel写入一个"事件"
}

也可以使用其他结构体作为自定义结构体中字段的类型,一般用于两个有关联的结构体中。

type Order struct {
	orderId  string
	userData User //指向上面定义的User结构体
	User          //也可以不提供字段的名字,只使用类型
}

func StructDemo2() {
	//访问二级结构体的字段
	var order Order
	fmt.Println(order.userData.Name)
	fmt.Println(order.User.Name)
	fmt.Println(order.Name) //可以省略掉二级结构体的名称,直接这样访问二级结构体的字段
}

不可以在结构体中包含类型为自身的字段,也不可以递归使用:

type T struct {
	t T
}
//报错:invalid recursive type: T refers to itself

type T1 struct {
	t2 T2
}
type T2 struct {
	t1 T1
}
//报错: invalid recursive type T1

但是却可以拥有自身类型的指针类型、以自身类型为元素类型的切片类型、以及以自身类型作为 value 类型的 map 类型的字段:

type TA struct {
	t  *TA           // ok
	st []TA          // ok
	m  map[string]TA // ok
}

结构体变量的声明和初始化

(1)零值初始化:使用结构体的零值(一个类型的默认值)作为它的初始值。比如:

var order Order //零值初始化

在 Go 语言标准库的代码中,sync 包的 Mutex 类型是用于多个并发 Goroutine 之间进行同步的互斥锁。通过 sync.Mutex 结构体的零值状态,开发者可直接基于零值状态下的 Mutex 进行 lock 与 unlock 操作,而且不需要额外显式地对它进行初始化操作。

Go 标准库中的 bytes.Buffer 结构体类型也是一个零值可用类型的例子。

var mu sync.Mutex
mu.Lock()
mu.Unlock()

var b bytes.Buffer
b.Write([]byte("Hello, Go"))
fmt.Println(b.String()) // 输出:Hello, Go

(2)赋值初始化:按顺序依次给每个结构体字段进行赋值(不推荐),或者指定字段名赋值(推荐)。

//按顺序依次给每个结构体字段进行赋值(不推荐)
var user1 = User{"zhangsan", 18, []string{"爬山", "跑步"}}

//指定字段名赋值(推荐)
var user2 = User{Name: "lisi", Age: 20, hobby: []string{"上网", "打球"}}
user2.Age = 99            //修改属性值
fmt.Println(user1, user2) //{zhangsan 18 [爬山 跑步]} {lisi 99 [上网 打球]}

//初始化空字段
user3 := User{}
fmt.Println(user3) //{ 0 []}

//使用 new 关键词,返回的是引用指针 (尽量少用)
user4 := new(User) //等价于 user4 := &User{}
fmt.Println(user4) //&{ 0 []}
//赋值
user4.Name = "rxbook"
user4.Age = 33
fmt.Println(user4)  //&{rxbook 33 []}
fmt.Println(*user4) //{rxbook 33 []}

//输出四种实例化的类型,获取指针就在前面加上&,获取指针对应的值就在前面加上*
fmt.Printf("user1:%T, user2:%T, &user3:%T, user4:%T", user1, user2, &user3, user4) //user1:main.User, user2:main.User, user3:*main.User, user4:*main.User

结构体的内存表示

结构体类型 T 在内存中布局非常紧凑,Go 为它分配的内存都用来存储字段了,没有插入的额外字段。可以使用标准库 unsafe 包提供的函数,获得结构体类型变量占用的内存大小,以及它每个字段在内存中相对于结构体变量起始地址的偏移量。

fmt.Println(unsafe.Sizeof(user2))        // 48 //结构体类型变量占用的内存大小
fmt.Println(unsafe.Offsetof(user2.Name)) // 0 //字段Name在内存中相对于变量 user2 起始地址的偏移量

Go 语言中结构体类型的大小受内存对齐约束的影响,不同的字段排列顺序也会影响到“填充字节”的多少,从而影响到整个结构体大小。比如下面两个结构体类型表示的抽象是相同的,但正是因为字段排列顺序不同,导致它们的大小也不同。所以,在日常定义结构体时,一定要注意结构体中字段顺序,尽量合理排序,降低结构体对内存空间的占用。

type T struct {
	b byte
	i int64
	u uint16
}
type S struct {
	b byte
	u uint16
	i int64
}
var t T
fmt.Println(unsafe.Sizeof(t)) // 24
var s S
fmt.Println(unsafe.Sizeof(s)) // 16

【方法】

Go 语言中的方法(method)和面向对象中的方法并不是一样的,因为Go并不支持经典的面向对象语法元素,比如类、对象、继承,等等。Go语言中的方法本质上也是函数,只是多了一个receiver 参数。比如 Go 标准库 net/http 包中 *Server 类型的方法:

// $GOROOT/src/net/http/server.go
func (srv *Server) ListenAndServeTLS(certFile, keyFile string) error {

}

receiver 参数

Go 中的方法必须是归属于一个类型的,而 receiver 参数的类型就是这个方法归属的类型,这里的receiver可以叫做“方法接收器”。

上面示例的 receiver 参数 srv 的类型为 *Server,那么这个方法就是 *Server 类型的方法。(注意,是 *Server

再来看一个方法的例子:

//go01/method1.go

package main

import "fmt"

type Users struct {
	Name  string
	Age   int
	hobby []string
}

func (user *Users) SetUserName(name string) {
	user.Name = name
}

func (user Users) GetUserName() {
	fmt.Printf("用户名是:%v \n", user.Name)
}

func main() {
	// 通过类型 Users 的变量实例调用方法
	var user1 Users
	user1.SetUserName("张三")
	user1.GetUserName() //用户名是:张三

	// 通过类型 *Users 的变量实例调用方法
	user2 := &Users{}
	user2.SetUserName("李四")
	user2.GetUserName() //用户名是:李四

	// 直接通过 Users 结构体调用方法并传入实例
	var user3 Users
	(*Users).SetUserName(&user3, "王五")
	Users.GetUserName(user3) //用户名是:王五
}

 上面 Users 示例中的两个方法可以转换为普通函数:

// SetUserName()的等价函数
func SetName(user *Users, name string) {
	user.Name = name
}

// GetUserName()的等价函数
func GetName(user *Users) {
	fmt.Printf("用户名是:%v \n", user.Name)
}

receiver参数的约束

每个方法只能有一个 receiver 参数,并且receiver 参数名字要保证唯一。Go 不支持在方法的 receiver 部分放置包含多个 receiver 参数的参数列表或者变长 receiver 参数。receiver 部分的参数名不能与方法参数列表中的形参名以及返回值中的变量名存在冲突,必须在这个方法的作用域中具有唯一性。比如下面的代码就会报错:

type T struct{}

func (t T) M(t string) { // 编译器报错:duplicate argument t (重复声明参数t)
    //... ...
}

如果在方法体中没有用到 receiver 参数,那么也可以省略 receiver 的参数名(用得不多):

type T struct{}

func (T) M(t string) {
    //... ...
}

receiver 参数的基类型本身不能为指针类型或接口类型。比如下面的例子会报错:

type MyInt *int

func (r MyInt) String() string { // r 的基类型为MyInt,编译器报错:invalid receiver type MyInt (MyInt is a pointer type)
	return fmt.Sprintf("%d", *(*int)(r))
}

type MyReader io.Reader

func (r MyReader) Read(p []byte) (int, error) { // r 的基类型为MyReader,编译器报错:invalid receiver type MyReader (MyReader is an interface type)
	return r.Read(p)
}

方法声明要与 receiver 参数的基类型声明放在同一个包内,因此不能为原生类型( int、float64、map 等)添加方法,也不能跨越 Go 包为其他包的类型声明新方法。

//试图为 Go 原生类型 int 增加新方法 Foo
func (i int) Foo() string { // 编译器报错:cannot define new methods on non-local type int
	return fmt.Sprintf("%d", i)
}

//试图跨越包边界,为 Go 标准库中的 http.Server 类型添加新方法Foo
func (s http.Server) Foo() { // 编译器报错:cannot define new methods on non-local type http.Server

}

方法的深入理解

因为方法自身的类型就是一个普通函数的类型,因此可以将方法作为右值赋值给一个函数类型的变量。

//go01/method1.go

package main

import "fmt"

type Users struct {
	Name  string
	Age   int
	hobby []string
}

func (user *Users) SetUserName(name string) {
	user.Name = name
}

func (user Users) GetUserName() {
	fmt.Printf("用户名是:%v \n", user.Name)
}

func main() {
	var user4 Users
	f1 := (*Users).SetUserName
	f2 := Users.GetUserName
	fmt.Printf("f1的类型是: %T \n", f1) //f1的类型是: func(*main.Users, string)
	fmt.Printf("f2的类型是: %T \n", f2) //f2的类型是: func(main.Users)
	f1(&user4, "赵六")
	f2(user4) //用户名是:赵六
}

goroutine中方法的使用

先来看一个例子,分析一下问题:

//go02/method2.go

package main

import (
	"fmt"
	"time"
)

type rxbook struct {
	name string
}

func (rx *rxbook) print() {
	fmt.Print(rx.name, " ") //输出结果: mongodb mongodb php mongodb golang java (顺序可能不一致,是由于 Goroutine 调度顺序不同,下同)
}

func main() {
	data1 := []*rxbook{{"php"}, {"golang"}, {"java"}}
	for _, v := range data1 {
		go v.print() //等价于下面一行代码:
		//go (*rxbook).print(v)
	}
	data2 := []rxbook{{"mysql"}, {"redis"}, {"mongodb"}}
	for _, v := range data2 {
		go v.print() //等价于下面一行代码:
		//go (*rxbook).print(&v)
	}
	time.Sleep(time.Second * 1)
	println()

}

发现上面输出的结果中,data2的结果只输出了三次“mongodb”,分析如下:

  • 迭代 data1 时,由于 data1 中的元素类型是 rxbook 指针 (*rxbook),因此赋值后的 v 就是元素地址,与 print() 方法的 receiver 参数类型(*rxbook)相同,每次调用 (*rxbook).print() 函数时直接传入的 v 即可,实际上传入的也是各个 rxbook 元素的地址;
  • 迭代 data2 时,由于 data2 中的元素类型是 rxbook(非指针),与 print()方法 的 receiver 参数类型(*rxbook) 不同,因此需要将其取地址后再传入 (*rxbook).print 函数,这样每次传入的 &v 实际上是变量 v 的地址,而不是切片 data2 中各元素的地址。这里的 v 在整个 for range 过程中只有一个,因此 data2 迭代完成之后 v 是元素“mongodb”的拷贝;
  • 因此,一旦启动各个子 goroutine 在 main goroutine 执行到 Sleep 时才被调度执行,data2中的子三个 goroutine 实际打印的是在 v 中存放的值“mongodb”,data1中的三个子 goroutine 各自传入的是元素“php”、“golang”和“java”的地址,所以打印的就没问题了。

如果想要让data2也能按照设定的三个值输出,只需要修改 print() 方法即可,将  print() 方法的 receiver 类型由 *rxbook 改为 rxbook 即可。

func (rx rxbook) print() {
	fmt.Print(rx.name, " ") //输出结果:mongodb mysql redis php java golang
}

通过上面的案例可以看出 receiver 参数类型对 Go 方法还是有很大影响的,具体是怎么影响的?往下看。

receiver 参数类型如何选择?T还是*T?

由于Go语言方法的本质就是以 receiver 参数作为第一个参数的普通函数,Go 函数的参数采用的是值拷贝传递。当 receiver 参数的类型为 rxbook 时,实际上传递的是结构体实例的一个副本,对此副本的所有修改都不会影响到原类型实例;而当 receiver 参数的类型为 *rxbook 时,实际上传递的是结构体实例的地址,因此所有修改都会影响到原来的类型实例。下面再看一个例子:

// go02/method3.go
package main

import "fmt"

type users struct {
	age int
}

// 使用 users 类型,传递的是副本,不会影响原数据
func (u users) setAge1(age int) {
	u.age = age
}

// 使用 *users 类型,传递的是原数据的地址,会影响原数据
func (u *users) setAge2(age int) {
	u.age = age
}

func main() {
	// 1.通过 users 调用
	var u1 users
	fmt.Println(u1.age) // 0
	u1.setAge1(18)
	fmt.Println(u1.age) // 0
	u1.setAge2(18)      //u1 的类型为 users,与 setAge2() 的 *users 不一致,会自动转换为下面的格式
	// (&u1).setAge2(18)   //与上面一行等价
	fmt.Println(u1.age) // 18
	println()

	// 2.通过 *users 调用
	var u2 = &users{}
	fmt.Println(u2.age) // 0
	u2.setAge1(25)      // u2 的类型为 *users,与 setAge1() 的 users 不一致,会自动转换为下面的格式
	// (*u2).setAge1(25)   //与上面一行等价
	fmt.Println(u2.age) // 0
	u2.setAge2(25)
	fmt.Println(u2.age) // 25
}
  • Go 判断 u1 的类型为 users,与方法 setAge2() 的 receiver 参数类型 *users 不一致,就会自动将 u1.setAge2() 转换为 (&u1).setAge2()
  • Go 判断 u2 的类型为 *users,与方法 setAge1() 的 receiver 参数类型 users 不一致,就会自动将 u2.setAge1() 转换为 (*u2).setAge1() ​​
  • 因此,无论是 users 类型实例,还是 *users 类型实例,都既可以调用 receiver 为 users 类型的方法,也可以调用 receiver 为 *users 类型的方法。

如果 Go 方法 要把对 receiver 参数代表的类型实例的修改,反映到原类型实例上,那么我们应该选择 *users 作为 receiver 参数的类型。

如果不需要在方法中对类型实例修改,那么应该尽量选择 users 类型进行数据拷贝,因为可以尽量少的暴露原类型;但是考虑到 receiver 参数是以值拷贝的形式传入方法中的,如果 receiver 参数类型的 size 较大,以值拷贝形式传入就会导致较大的性能开销,这时选择 *users 作为 receiver 类型可能更好些。

方法集合

先来看一段代码:

// go02/method4.go
package main

import (
	"fmt"
	"reflect"
)

type userInterface interface {
	SetAge1()
	SetAge2()
}

type users struct {
	age int
}

func (u users) SetAge1() { // 实现自userInterface接口,并且基类是 users

}

func (u *users) SetAge2() { // 实现自userInterface接口,并且基类是 *users

}

func main() {
	var u1 users
	var u2 *users
	var i userInterface
	fmt.Println(u1, u2, i) //{0} <nil> <nil>

	i = u2 //正常
	i = u1 //报错: users 没有实现 userInterface 类型方法列表中的 setAge2,因此类型 users 的实例 u1 不能赋值给 userInterface 变量
}

为什么 *users 类型的 u2 可以正常赋值给 userInterface 类型变量 i,而 users 类型的 u1 就不行呢?先等等往下看。

方法集合 是用来判断一个类型是否实现了某接口类型的唯一办法。Go 中任何一个类型都有属于自己的方法集合,对于没有定义方法的 Go 类型(比如 int类型)可以称其拥有空方法集合。

// go02/method5.go
package main

import (
	"fmt"
	"reflect"
)

// 自定义一个结构体 T
type T struct{}

func (t T) M1()  {} //实现自Interface接口,基类是T
func (t T) M2()  {}
func (t *T) M3() {} //实现自Interface接口,基类是*T
func (t *T) M4() {}

func dumpMethodSet(i interface{}) {
	dynTyp := reflect.TypeOf(i)
	if dynTyp == nil {
		fmt.Printf("没有动态类型 \n")
		return
	}

	n := dynTyp.NumMethod()
	if n == 0 {
		fmt.Printf("%s 的方法集合为空\n", dynTyp)
		return
	}
	fmt.Printf("%s 的方法集合: ", dynTyp)
	for j := 0; j < n; j++ {
		fmt.Print(dynTyp.Method(j).Name, ", ")
	}
	fmt.Printf("\n")
}
func main() {
	// Go 原生类型的 int、*int 由于没有定义方法,所以它们的方法集合都是空的
	var n int
	dumpMethodSet(n)  //int 的方法集合为空
	dumpMethodSet(&n) //*int 的方法集合为空

	// 自定义类型 T 定义了方法 M1 和 M2,因此它的方法集合包含了 M1 和 M2
	// *T 的方法集合中除了包含 M3 和 M4,也包含了类型 T 的方法 M1 和 M2
	var t1 T
	dumpMethodSet(t1)  //main.T 的方法集合: M1, M2,
	dumpMethodSet(&t1) //*main.T 的方法集合: M1, M2, M3, M4,
}

现在把上面代码中的u1和u2使用 dumpMethodSet 方法打印出来看看:

// go02/method4.go
func main() {
	var u1 users
	var u2 *users
	
	dumpMethodSet(u1) //main.users 的方法集合: SetAge1,
	dumpMethodSet(u2) //*main.users 的方法集合: SetAge1, SetAge2,
}

通过打印输出的结果可以理解上面 *users 类型的 u2 为什么可以正常赋值给 userInterface 类型变量 i,因为 *users 的方法集合中包含了 users 的方法,而反过来却不包含。 

结论:

  • *T 类型的方法集合包含所有以 *T 为 receiver 参数类型的方法,以及所有以 T 为 receiver 参数类型的方法。
  • 所以,设置receiver参数的类型到底应该是 T 还是 *T 可以参考下面的原则:T 类型是否要实现某一接口。即:如果 T 类型需要实现某个接口,就要使用 T 作为 receiver 参数;如果 *T 类型需要实现某个接口,可以使用 *T 作为 receiver 参数(传递的是地址,所有修改都会影响到原实例),也可以使用 T 作为 receiver 参数(传递的是副本,不会影响原类型实例)。

【指针】

指针是一个指向某个确切的内存地址的值,这个内存地址可以是任何数据或代码的起始地址,比如某个变量、某个字段或某个函数。

关于Go语言的指针:*表示指针,&取地址。如果声明了一个指针但是没有赋值,那么它的默认值就是 nil。

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

func main() {
	var dog *Dog = &Dog{name: "大黄"}
	fmt.Println(dog)  //&{大黄}
	fmt.Println(*dog) //{大黄}
	fmt.Println(&dog) //0xc000012028

	dog2 := Dog{"小黑"}
	fmt.Println(dog2)  //{小黑}
	fmt.Println(&dog2) //&{小黑}
	println()
}

对于基本类型Dog来说,*Dog就是它的指针类型。而对于一个Dog类型,值不为nil的变量dog,取址表达式 &dog 的结果就是该变量的值(也就是基本值)的指针值。如果一个方法的接收者是 *Dog 类型的,那么该方法就是基本类型Dog的一个指针方法。

指针的指针:

a := 10
var p1 *int
var p2 **int
fmt.Println(p2) //<nil>
p2 = &p1
fmt.Printf("%T,%T,%T\n", a, p1, p2) //int, *int, **int
fmt.Println("p2的数值,也就是p1的地址:", p2)  //0xc000120020
fmt.Printf("p2的地址:%p\n", &p2)       //0xc000120028

结构体内部引用自己,只能使用指针。或者说,在整个引用链上,如果构成循环那就只能用指针。

type Node struct {
	//left Node
	//right Node

	left  *Node
	right *Node

	// 这个也会报错
	// nn NodeNode
}
type NodeNode struct {
	node Node
}

uintptr和unsafe.Pointer

uintptr 是一个 Go 语言内建的数据类型,实际上也是一个数值类型,可以代表“指针”。根据当前计算机的计算架构的不同,它可以存储 32 位或 64 位的无符号整数,可以代表任何指针的位(bit)模式,也就是原始的内存地址。

Go 语言标准库中的unsafe包中有一个类型叫做Pointer,也代表了“指针”。unsafe.Pointer可以表示任何指向可寻址的值的指针,通过 unsafe.Pointer 可以在普通指针和uintptr值之间进行双向的转换。

把指针值转换成uintptr类型的值有什么意义吗? 

  • 一个指针值(比如*Dog类型的值)可以被转换为一个unsafe.Pointer类型的值,反过来也是如此。
  • 一个uintptr类型的值也可以被转换为一个unsafe.Pointer类型的值,反过来也是如此。
  • 一个指针值无法被直接转换成一个uintptr类型的值,反过来也是如此。

怎样通过unsafe.Pointer操纵可寻址的值?

type Dog struct {
	name string
}

func main() {
	dog3 := Dog{"泰迪"}                                       //声明一个Dog类型的变量dog3
	dog3Pointer := &dog3                                    //然后用取址操作符&取出它的指针值,并赋给了变量dog3Pointer
	dog3PointerData := uintptr(unsafe.Pointer(dog3Pointer)) //使用两个类型转换,先把dog3Pointer 转换成了一个unsafe.Pointer类型的值,然后继续转换成了一个uintptr的值
	fmt.Println(dog3PointerData)                            //824634142288

	namePtr := dog3PointerData + unsafe.Offsetof(dog3Pointer.name) //unsafe.Offsetof函数用于获取两个值在内存中的起始存储地址之间的偏移量,以字节为单位
	nameP := (*string)(unsafe.Pointer(namePtr))                    //把这个偏移量 和 结构体值在内存中的起始存储地址(dog3PointerData)相加就可以得到dog3Pointer.name字段值的起始存储地址
	fmt.Println(nameP)                                             //0xc000066e50
	fmt.Println(*nameP)                                            //泰迪
	fmt.Println(&(dog3Pointer.name))                               //0xc000066e50,虽然这样也能拿到内存地址,但是如果根本就不知道这个结构体类型是什么,也拿不到dog3Pointer这个变量呢?
}

通过上面的 namePtr 可以知道,它是一个无符号整数,但同时也是一个指向了程序内部数据的内存地址。它可能会给带来一些好处,比如可以直接修改埋藏得很深的内部数据。但如果一旦不小心把这个内存地址泄露出去,那么其他人就能够轻而易举的改动 dog3Pointer.name 的值,以及周围的内存地址上存储的任何数据了,即使他们不知道这些数据的结构也无所谓,可以随便乱改,不正确地改动一定会给程序带来不可预知的问题,甚至造成程序崩溃。所以,使用这种非正常的手段会很危险,一定要谨慎使用。

不可寻址的值无法使用取址操作符&获取它们的指针,对不可寻址的值获取地址操作都会使编译器报错。

package main

import "fmt"

type Dog struct {
	name string
}

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

func main() {
	fmt.Println(Dog{"小黑"}.SetName("汪汪")) //报错: cannot call pointer method SetName on Dog
}

【问】Go 语言中的哪些值是不可寻址的?

  • 不可变的值不可寻址。常量、基本类型的值字面量、字符串变量的值、函数以及方法的字面量都是如此。其实这样规定也有安全性方面的考虑。
  • 绝大多数被视为临时结果的值都是不可寻址的。算术操作的结果值属于临时结果,针对值字面量的表达式结果值也属于临时结果。但有一个例外,对切片字面量的索引结果值虽然也属于临时结果,但却是可寻址的。
  • 若拿到某值的指针可能会破坏程序的一致性,那么就是不安全的,该值就不可寻址。由于字典的内部机制,对字典的索引结果值的取址操作都是不安全的。另外,获取由字面量或标识符代表的函数或方法的地址显然也是不安全的。

源代码:https://gitee.com/rxbook/go-demo-2023/tree/master/basic/go02

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

浮尘笔记

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值