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

news2024/11/25 0:45:21

目录

【定义新数据类型】

【结构体】

定义结构体

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

结构体的内存表示

【方法】

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/534866.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

9. 三星索引和Mysql内核查询成本计算实战

MySQL性能调优 1. 高性能的索引创建策略1.1 只为用于搜索、排序或分组的列创建索引1.2 合理设计多列索引1.3 尽可能设计三星索引1.4 主键尽量是很少改变的列1.5 处理冗余和重复索引1.6 删除未使用的索引1.7 InnoDB中的索引 2. 补充资料&#xff1a;磁盘和B树Mysql内核查询成本计…

从0到1无比流畅的React入门教程

无比流畅的React入门教程TOC React 是什么 简介 用于构建 Web 和原生交互界面的库React 用组件创建用户界面通俗来讲&#xff1a;是一个将数据渲染为HTML视图的开源JS库 其他信息 Facebook 开发&#xff0c;并且开源 为什么使用React? 原生JS使用DOM-API修改UI代码很繁…

使用Appium实现录制回放

1、cmd中转到abd所在位置&#xff1a; cd C:\Users\lenovo\AppData\Local\Android\Sdk\platform-tools 2、打开Appium运行 3、打开Appium Inspector &#xff08;1&#xff09;获取设备名称 在cmd中输入以下命令&#xff1a; adb devices &#xff08;2&#xff09;获取appP…

c++核心知识—多态

目录 一、多态 1、多态的基本概念 2、深入剖析多态原理&#xff1a; 3、纯虚函数和抽象类 4、虚析构和纯虚析构 一、多态 1、多态的基本概念 多态是C面向对象三大特性之一 多态分为两类&#xff1a; 1、静态多态: 函数重载 和 运算符重载 属于静态多态&#xff0c;复用…

前端八股文(二)

1.什么是diff算法&#xff1f; https://www.bilibili.com/video/BV1JR4y1R7Ln/?spm_id_from333.337.search-card.all.click&vd_source0406fa5cf8203ba41f1c8aec5f967e9d 我们修改了文本内容后会生成新的虚拟dom,新旧俩个虚拟dom之间是存在一定差异的&#xff0c;如果能快…

未来的航空电子设备会是什么样子?

具有多功能航空电子设备、大触摸屏显示器、先进通信系统、高性能/低功耗解决方案和人工智能 (AI) 功能的驾驶舱将成为未来军事飞行员日常生活的一部分。 如今&#xff0c;配备模拟驾驶舱的军用飞机已经很少见&#xff0c;因为大多数都已被采用先进嵌入式硬件和软件解决方案的现…

金融学学习笔记第2章

第2章 金融市场和金融机构 一、金融体系 金融体系包括金融市场、中介、服务公司和其它用于执行家庭、企业及政府的金融决策的机构 1.金融市场 金融市场&#xff1a;以金融资产为交易对象而形成的供求关系及其机制的总和 金融市场可分为有特定地理位置的市场和没有特定地点的市…

使用NodeJs创建Web服务器

Web服务器 什么是Web服务器&#xff1f; 当应用程序&#xff08;客户端&#xff09;需要某一个资源时&#xff0c;可以向一个台服务器&#xff0c;通过Http请求获取到这个资源&#xff1b;提供资源的这个服务器&#xff0c;就是一个Web服务器&#xff1b; 目前有很多开源的We…

用ChatGPT生成一个Python贪吃蛇游戏(42)

小朋友们好&#xff0c;大朋友们好&#xff01; 我是猫妹&#xff0c;一名爱上Python编程的小学生。 和猫妹学Python&#xff0c;一起趣味学编程。 今日主题 什么是ChatGPT&#xff1f; ChatGPT可以帮我们做什么&#xff1f; 用ChatGPT生成一个Python贪吃蛇游戏。 什么是C…

Rust每日一练(Leetday0002) 中位数、回文子串、Z字形变换

目录 4. 寻找两个正序数组的中位数 Median of two sorted arrays &#x1f31f;&#x1f31f;&#x1f31f; 5. 最长回文子串 Longest Palindromic Substring &#x1f31f;&#x1f31f; 6. Z字形变换 Zigzag Conversion &#x1f31f;&#x1f31f; &#x1f31f; 每日…

C/C++每日一练(20230517) 排序问题、查找小值、寻找峰值

目录 1. 排序问题 &#x1f31f; 2. 查找小值 &#x1f31f; 3. 寻找峰值 &#x1f31f;&#x1f31f; &#x1f31f; 每日一练刷题专栏 &#x1f31f; Golang每日一练 专栏 Python每日一练 专栏 C/C每日一练 专栏 Java每日一练 专栏 1. 排序问题 输入10个数&#…

RK3588平台开发系列讲解(进程篇)Linux文件系统数据结构

平台内核版本安卓版本RK3588Linux 5.10Android 12文章目录 一、Linux 文件系统数据结构有哪些二、超级块结构 spuer_block三、目录 dentry四、文件索引结点 inode五、打开的文件 file沉淀、分享、成长,让自己和他人都能有所收获!😄 📢 本篇将介绍 Linux 文件系统数据结构…

深入理解MySQL中的事务和锁

目录 数据库中的事务是什么&#xff1f; MySQL事务的隔离级别 脏读、不可重复读、幻读 MVCC&#xff08;多版本并发控制&#xff09; 快照读和当前读 MySQL中的锁 MyISAM引擎的锁&#xff1a; InnoDB引擎的锁&#xff1a; 乐观锁和悲观锁 共享锁和排他锁 数据库中的事…

【STL二十】算法——排序操作(sort、stable_sort)_集合操作(merge)

【STL二十】算法——排序操作(sort、stable_sort&#xff09;_ 集合操作(merge&#xff09; 一、分类二、修改序列的操作三、排序操作1、sort2、stable_sort3、is_sorted、is_sorted_until 四、集合操作1、merge2、inplace_merge 一、分类 根据网站https://www.apiref.com/cpp…

JavaScript实现输出一个“天”字的代码

以下为实现输出一个“天”字的程序代码和运行截图 目录 前言 一、实现输出一个“天”字 1.1 运行流程及思想 1.2 代码段 1.3 JavaScript语句代码 1.4 运行截图 前言 1.若有选择&#xff0c;您可以在目录里进行快速查找&#xff1b; 2.本博文代码可以根据题目要求实现相…

【瑞萨RA系列FSP库开发】初识寄存器

文章目录 一、寄存器是什么二、瑞萨RA6M5 芯片内部模块与资源三、存储器映射1. 存储器映射表2. 存储器区域划分3. 外设寄存器 四、C语言操作寄存器1. C语言对寄存器的封装&#xff08;1&#xff09;外设模块基地址定义&#xff08;2&#xff09;寄存器结构体定义&#xff08;3&…

【LLM系列之BLOOM】BLOOM: A 176B-Parameter Open-Access Multilingual Language Model

论文题目&#xff1a;《BLOOM: A 176B-Parameter Open-Access Multilingual Language Model》 论文链接&#xff1a;https://arxiv.org/abs/2211.05100 github链接&#xff1a;https://github.com/huggingface/transformers-bloom-inference/tree/main huggingface链接&#xf…

LeetCode35. 搜索插入位置(二分法入门)

写在前面&#xff1a; 题目链接&#xff1a;LeetCode35. 搜索插入位置 编程语言&#xff1a;C 题目难度&#xff1a;简单 一、题目描述 给定一个排序数组和一个目标值&#xff0c;在数组中找到目标值&#xff0c;并返回其索引。如果目标值不存在于数组中&#xff0c;返回它将会…

Linkage Mapper:从栖息地连通性到物种保护的连通性指南针

✅创作者:陈书予 🎉个人主页:陈书予的个人主页 🍁陈书予的个人社区,欢迎你的加入: 陈书予的社区 🌟专栏地址: Linkage Mapper解密数字世界链接 文章目录 引言一、Linkage Mapper工具包简介1.1 什么是Linkage Mapper工具包⭐️ Linkage Mapper工具包的概述

Vite4 + Vue3 项目打包并发布Nginx服务器 (前端必看)

一、环境 &#x1f604; &#x1f604; &#x1f604; 这里因为我们有的小伙伴可能不太需要服务器&#xff0c;单纯学习的话也没有必要去买一个服务器。如果需要把自己的东西部署到公网上&#xff0c;有很多方式&#xff0c;自行百度。你也可以购买阿里云或者腾讯云。逻辑都是…