Golang 对 对象和指针 的理解
1、定义一个结构体
type User struct { userid int username string password string }
2、初始化一个结构体
有两种情况,一是得到结构体的对象,一是得到结构的对象指针,分别有三种方式:
//第1种方式,先声明对象,再初始化 var player1 Player player1.userid = 1 player1.username = "lina1" player1.password = "123456" //第2种方式,声明同时初始化 player2 := Player{2, "lina2", "123456"} //第3种方式,通过 field:value 形式初始化,该方式可以灵活初始化字段的顺序 player3 := Player{username: "lina3", password: "123456", userid: 3} //上面三种初始化方式都是生产对象的,相应如果想初始化得到对象指针的三种方法如下: //第1种方式,使用 new 关键字 player4 := new(Player) player4.userid = 4 player4.username = "lina4" player4.password = "123456" //第2种方式,声明同时初始化 player5 := &Player{5, "lina2", "123456"} //第3种方式,通过 field:value 形式初始化,该方式可以灵活初始化字段的顺序 player6 := &Player{username: "lina3", password: "123456", userid: 6}
3、对象与对象指针的区别(更确切的说应该是值类型和指针类型)
与C/C++类似,GO语言也存在对象与对象的指针,但不同的是,GO语言中没有 -> 操作符来调用指针所属的成员,而与一般对象一样,都是使用 . 来调用。
对于一个函数,如果函数或者方法(函数和方法是由区别的,详见https://www.cnblogs.com/maji233/p/11060471.html)的参数(或接收者)是对象指针时,表示此对象是可被修改的;相反的,如果是对象时,表示是不可修改的,因为参数是副本,是实参的一份拷贝(但如果该对象本身就是指针类型,如 map、func、chan、slice等,则本质上是可以修改的)。所以一般的做法是,方法的接收者习惯性使用对象指针,而不是对象,一方面可以在想修改对象时进行修改,另一方面也减少参数传递的拷贝成本。
另外,有一点尤为特殊,如果是作为函数的参数,则函数定义时,是使用对象还是对象指针,是有本质区别的,在使用对象作为参数的函数中,不能传入对象指针,同样的,在使用对象指针作为参数的函数中,也不能传入对象,否则编译器会报错。但如果是方法(指结构体绑定的方法),则接收者定义为对象还是对象指针,都可以接收对象和对象指针的调用。
下面我们来定义相关的函数和方法如下:
func print_obj(player Player) { //player.username = "new" //修改并不会影响传入的对象本身 log.Println("userid:", player.userid) } //传入 Player 对象指针参数 func print_ptr(player *Player) { player.username = "new" log.Println("userid:", player.userid) } //接收者为 Player 对象的方法,方法接收者的变量,按照 GO 语言的习惯一般不用 this/self ,而是使用接收者类型的第一个小写字母,可以看标准库中的代码风格。 func (p Player) m_print_obj() { //p.username = "new" //修改并不会影响传入的对象本身 log.Println("self userid:", p.userid) } //接收者为 Player 对象指针的方法 func (p *Player) m_print_ptr() { p.username = "new" log.Println("self userid:", p.userid) }
然后测试一下函数跟方法的调用:
//print_ptr(player2) //无法调用,编译出错 player2.m_print_obj() player2.m_print_ptr() //print_obj(player6) //无法调用,编译出错 print_ptr(player6) player6.m_print_obj() player6.m_print_ptr()
既然对于对象与对象指针的区别,方法的处理很特殊,那么将一个对象传入到接收者为对象指针的方法中,及将一个对象指针传入到一个接收者为对象的方法中,能不能修改传入对象的值呢?答案是,由方法的定义决定,而不是方法的调用者类型决定。
4、匿名字段
结构体里的字段可以只有类型名,而没有字段名,这种字段称为匿名字段。匿名字段可以是一个结构体、切片等复合类型,也可以是 int 这样的简单类型。但建议不要把简单类型作为匿名字段。
type Pet struct { id int petname string } type Player struct { id int Pet int } func main() { var player1 Player player1.petname = "pet1" //可以直接访问匿名字段中的成员,就像访问自己的成员一样 player1.int = 3 //一般不推荐将简单类型作为匿名字段,如果有多个匿名的int,这里就没法处理了 player1.id = 1 //如果外层跟内层字段名重复的话,优先取外层字段 player1.Pet.id = 10 //如果外层跟内层字段名重复的话,可以通过这种形式来访问内层字段 }
一个命名为S的结构体类型将不能再包含S类型的成员:因为一个聚合的值不能包含它自身。(该限制同样适应于数组。)但是S类型的结构体可以包含*S
指针类型的成员,这可以让我们创建递归的数据结构,比如链表和树结构等。
如果结构体没有任何成员的话就是空结构体,写作struct{}。它的大小为0,也不包含任何信息,但是有时候依然是有价值的。有些Go语言程序员用map带模拟set数据结构时,用它来代替map中布尔类型的value,只是强调key的重要性,但是因为节约的空间有限,而且语法比较复杂,所有我们通常避免避免这样的用法。
seen := make(map[string]struct{}) // set of strings // ... if _, ok := seen[s]; !ok { seen[s] = struct{}{} // ...first time seeing s... }
结构体可以作为函数的参数和返回值,如果结构体较大,一般使用指针参数,而且如果要在函数修改结构体,则必须使用指针形式。go语言中所有的函数参数都是值拷贝。
如果结构体的全部成员都是可比较的,则该结构体也可比较,则可作为Map的key类型。
得意于匿名嵌入的特性,我们可以直接访问叶子属性而不需要给出完整的路径:
var w Wheel w.X = 8 // equivalent to w.Circle.Point.X = 8 w.Y = 8 // equivalent to w.Circle.Point.Y = 8 w.Radius = 5 // equivalent to w.Circle.Radius = 5 w.Spokes = 20
不幸的是,结构体字面值并没有简短表示匿名成员的语法, 因此下面的语句都不能编译通过:
w = Wheel{8, 8, 5, 20} // compile error: unknown fields w = Wheel{X: 8, Y: 8, Radius: 5, Spokes: 20} // compile error: unknown fields
到目前为止,我们看到匿名成员特性只是对访问嵌套成员的点运算符提供了简短的语法糖。稍后,我们将会看到匿名成员并不要求是结构体类型;其实任何命令的类型都可以作为结构体的匿名成员。但是为什么要嵌入一个没有任何子成员类型的匿名成员类型呢?
答案是匿名类型的方法集。简短的点运算符语法可以用于选择匿名成员嵌套的成员,也可以用于访问它们的方法。实际上,外层的结构体不仅仅是获得了匿名成员类型的所有成员,而且也获得了该类型导出的全部的方法。这个机制可以用于将一个有简单行为的对象组合成有复杂行为的对象。组合是Go语言中面向对象编程的核心。
结体体定义时,可以为每一个字段添加一个 Tag,比如使用内置Json库时,就可能用到这个Tag。具体看元数据和反射。