共计 6536 个字符,预计需要花费 17 分钟才能阅读完成。
Go 语言——Struct 和 Method
[TOC]
struct 特点
使用 type <Name> struct{} 定义结构,名称遵循可见性规则
struct 是值类型
可以使用字面值对结构进行初始化
支持匿名结构,和匿名字段
允许直接通过指针来读写结构成员
相同类型的成员可进行直接拷贝赋值,支持 == 与 != 比较运算符,但不支持 > 或 <
嵌入结构作为匿名字段看起来像继承,但不是继承
Go 中的 struct 没有构造函数,一般可以使用工厂模式来解决这个问题
一、结构体 struct
1、struct 介绍
struct 首先是一种类型,值类型。它是由一系列具有相同类型或不同类型的数据构成的数据集合。和 c 语言的 struct 很像,其实就相当于 java 的 class,但是 go 是没有 class 概念的。定义类型的目的其实就是告诉编译器要开辟多少内存,内存中放什么类型,而结构体组合多个类型。
2、struct 定义和初始化
type person struct{// 定义一个 person struct,
Name string
Age int
}
func main(){
a:=person{}// 初始化这个 struct,这个时候会用类型零值进行初始化
fmt.Println(a)
a.Name=”tom” // 对结构体的属性进行赋值
a.Age=19
fmt.Println(a)
}
// 打印结果
{0}
{tom 19}
从上面的例子看到,我们可以使用 type <Name> struct{} 来定义一个结构体,里面可以声明结构体拥有的属性。(结构体的名字遵循大小写访问权限)。然后,使用 a:=person{} 初始化了一个空的结构体,这是时候打印出的是 {空字符串 0},(初始化结构体时默认使用零值)。接下来可以通过. 来操作初始化了的结构体,访问它的属性进行赋值操作。
接下来列出 struct 初始化的方式
2.1 使用 var 关键字声明,并初始化为其零值
var Tom person // 关键字 var 创建了类型为 person 且名为 Tom 的变量
// 注意,这里会使用 person 各个属性的零值进行初始化
2.2 使用字面量来声明创建结构体变量,
第一种形式:
tom:=person{// 使用:= 创建了 person 结构体类型的变量 tom, 初始值是 tom, 和 18
Name:”Tom”,
Age:18,
}
// 注意:每一行必须以逗号结尾
第二种形式:
tom:=person{“Tom”,20} // 必须要和结构声明中字段的顺序一致
// 当然也可以这样 — 都是一样的,注意顺序就可以
tom:=person{
“Tom”,
20,
}
2.3 嵌套结构体
type student struct{
per person //person 结构体作为属性
grade string //student 的属性,
}
其实使用这种组合的方式,可以将不同的结构体组成一个集合,其实也是实现了面向对象的继承。
小结,struct 是值类型,赋值和传参会复制全部内容。可⽤ “_” 定义补位字段,⽀持指向⾃⾝类型的指针
3、结构体比较
struct ⽀持 “==”、”!=” 相等操作符,可⽤作 map 键类型。相等比较运算符 == 将比较两个结构体的每个成员
type Point struct{X, Y int}
p := Point{1, 2}
q := Point{2, 1}
fmt.Println(p.X == q.X && p.Y == q.Y) // “false”
fmt.Println(p == q) // “false”
既然可以比较,那么久可以作为 map 键类型
type User struct {
id int
name string
}
m := map[User]int{
User{1, “Tom”}: 100,
}
4、匿名字段
Go 语言有一个特性让我们只声明一个成员对应的数据类型而不指名成员的名字;这类成员就叫匿名成员。匿名成员的数据类型必须是命名的类型或指向一个命名的类型的指针。
type person struct{// 匿名字段
string
int
}
func main(){
tom:=person{“name”,19} // 顺序必须和声明一致
fmt.Println(tom)
}
3、结构体嵌入
type human struct {// 定义 human 的结构体
Sex int
}
type teacher struct {// 定义 teacher 结构体
human // 组合的形式,不存在继承直接放入 human
Name string
Age int
}
type student struct {
human // 自动以 human 命名匿名结构
Name string
Age int
}
func main() {
// 所以需要用 human 这个名称进行操作这个匿名结构
a := teacher{Name:”joe”,Age:18,human:human{Sex:0}}
b:= student{Name:”joe”,Age:18,human:human{Sex:0}}
a.Name=”aaaa”
a.Age=11
a.Sex=100 // 可以直接修改
fmt.Println(a,b)
}
从上面的例子可以看出,teacher 和 student 都是人,如果把 human 结构体作为他们的内部属性,那就相当于 teacher 和 student 拥有了 human 结构体的所有属性和方法,变相实现了继承。
二、Method
只能为当前包内命名类型定义方法。
参数 receiver 可任意命名。如方法中未曾使用,可省略参数名。
参数 receiver 类型可以是 T 或 *T。基类型 T 不能是接口或指针。
不支持方法重载,receiver 只是参数签名的组成部分。
可用实例 value 或 pointer 调用全部⽅法,编译器自动转换。
类型别名不会拥有底层类型所附带的方法
1、方法声名
type A struct{// 定义结构体 A
Name string
}
type B struct{// 定义结构体 B
Name string
}
func (a A) Print(){ // 结构体 A 的方法
fmt.Println(“A 的方法 ”)
}
func (b B) Print(){ // 结构体 B 的方法
fmt.Println(“B 的方法 ”)
}
func main(){
a:=A{}
a.Print()
b:=B{}
b.Print()
}
从上面的代码看出,方法实际上也是函数,只是在声明时,在关键字 func 和方法名之间增加了一个参数,
func (接收者变量名 接收者) 方法名 (){
}
receiver 声名的是谁,这个就是哪个结构体的方法。注意:只能为同一个包中的类型定义方法
没有构造和析构方法,通常用简单工厂模式返回对象实例。
type Person struct {
Name string
}
func setName() *Person { // 创建对象实例。
return &Person{}
}
func main(){
a:=setName()
a.Name=”zhangsan”
fmt.Println(a.Name)
}
2、值接收者和指针接收者
type Data struct {
x int
}
func (self Data) ValueTest() { // 值接受者方法
fmt.Printf(“ 值接收者: %p\n”, &self)
}
func (self *Data) PointerTest() { // 指针接受者方法
fmt.Printf(“ 指针接受者: %p\n”, self)
}
func main() {
d := Data{}
p := &d
fmt.Printf(“ 原始地址: %p\n”, p)
d.ValueTest() // ValueTest(d)
d.PointerTest() // PointerTest(&d)
p.ValueTest() // ValueTest(*p)
p.PointerTest() // PointerTest(p)
}
// 输出结果
原始地址: 0xc00005a058
值接收者: 0xc00005a090
指针接受者: 0xc00005a058
值接收者: 0xc00005a098
指针接受者: 0xc00005a058
这个实例可以看到,定义结构体的方法可以是值接收者,也可以是指针接收者。区别在于方法名前面的 receiver 一个是指针,一个是实际的值。
并且调用方法的时候,struct 变量的值可以调用指针接收者的方法,指针也可以调用值接受者的方法。分开来说这几种情况:
2.3 使用值调用值接收者方法
func (self Data) ValueTest() { // 值接受者方法
fmt.Printf(“ 值接收者: %p\n”, &self)
}
使用值来调用此方法,receiver 是 Datal 类型。当发起调用的时候,此方法操作的是值调用者的一个值的拷贝。如最上面的代码中,连续值调用最终打印出的地址是不同的,每次都不相同,因为每一次的调用,都是使用的值的拷贝,并且对这个值做任何操作,都不会影响初始值。
2.4 使用指针调用指针接收者方法
func (self *Data) PointerTest() { // 指针接受者方法
fmt.Printf(“ 指针接受者: %p\n”, self)
}
使用指针来调用此方法,receiver 是一个指向 Data 类型的指针。当调用使用指针接收者声明的方法时,这个方法会共享调用方法时接收者所指向的值。其实也就是,此方法直接可以操作接收者指针指向的底层数据的值。这样方法对值进行任何改变都会对原始值产生影响。类似引用传递。
2.5 使用指针调用值接收者方法
使用指针来调用值接收者方法就有意思了。值接收者前面说了是对值的拷贝进行操作,那么指针怎么样?其实这是 go 语言编译器做了处理,比如 p.ValueTest() 这段代码在编译器会修改为 (*p).ValueTest() , 也就是编译器会自动先取出指针指向的值,然后调用值接收者方法。注意:这里的调用也是一个值的拷贝,只不过拷贝的是指针指向的值。再次声明:此方法操作的是指针指向的值的副本。也就是,此方法怎么操作也不会影响原始值。
2.6 使用值调用指针接收者方法
使用值来调用指针接收者方法。这个一看很矛盾,指针怎么接受值。于是乎 go 的编译器再一次做了调整,如 d.PointerTest(), 经过编译器处理变为 (&d).PointerTest()。这就很明显了,其实就是语法糖,为了方便。编译器会先取出值的地址,然后使用此地址指向的值进行处理。操作的也是原始值。
总结下:值接收者使用值的副本来调用方法,而指针接受者使用实际值来调用方法。
3、匿名字段
可以像字段成员那样访问匿名字段⽅法,编译器负责查找
type User struct {//User 结构体
id int
name string
}
type Manager struct {
User // 匿名字段
}
func (self *User) ToString() string { //User 的方法
return fmt.Sprintf(“User: %p, %v”, self, self)
}
func main() {
m := Manager{User{1, “ 张大仙 ”}}
fmt.Printf(“ManagerL: %p\n”, &m)
fmt.Println(m.ToString())
}
// 输出
ManagerL: 0xc000054400
User: 0xc000054400, &{1 张大仙}
从上面代码看到,Manager 内部声明了 User 类型的匿名字段,那么 Manager 就具备了 User 的所有方法。
如果匿名字段的方法和外部结构的方法重名怎么办?其实还是就近原则,如果外部结构和嵌入结构存在同名方法,则优先调用外部结构的方法
由此可见,我们可以理解为:使用匿名字段,就相当于扩展了此 struct 的功能,也就是变相实现了继承,就可以直接调用匿名字段的方法。如果我们要实现方法重写,那么就可以在外层定义一个重名方法,修改方法内容,那不就是实现了方法重写吗!!!
type User struct {//User 结构体
id int
name string
}
type Manager struct {
User // 匿名字段
title string //Manager 特有的字段
}
func (self *User) ToString() string { //user 的方法
return fmt.Sprintf(“User: %p, %v”, self, self)
}
func (self *Manager) ToString() string {//manager 的方法
return fmt.Sprintf(“Manager: %p, %v”, self, self)
}
func main() {
m := Manager{User{1, “ 张大仙 ”}, “ 标题 ”}
fmt.Println(m.ToString())
fmt.Println(m.User.ToString())
}
// 输出结果
Manager: 0xc00006e240, &{{1 张大仙} 标题 }
User: 0xc00006e240, &{1 张大仙}
3、Method Value 和 Method Expression
方法值和方法表达式,从某种意义上来说,方法是函数的语法糖,因为 receiver 其实就是方法所接收的第 1 个参数(Method Value vs. Method Expression)
根据调⽤者不同,⽅法分为两种表现形式:
instance.method(args…) —> <type>.func(instance, args…)
两者都可像普通函数那样赋值和传参,区别在于 method value 绑定实例,⽽ method expression 则须显式传参。
type TZ int // 自定义一个 int 类型的类型 TZ
func (a *TZ)Print(){
fmt.Println(“TZ”)
}
func main(){
var a TZ // 声明变量 a TZ 类型
a.Print() //Method Value 方式
(*TZ).Print(&a)// Method Expression 方式
}
这里借用下 go 学习笔记的总结,慢慢体会
方法集:
/*
每个类型都有与之关联的方法集,这会影响到接口实现规则
– 类型 T 方法集包含全部 receiver T 方法
– 类型 *T 方法集包含全部 receiver T+ * T 方法
– 如类型 S 包含匿名字段 T,则 S 方法集包含 T 方法
– 如类型 S 包含匿名字段 *T,则 S 方法集包含 T +* T 方法
– 不管嵌入 T 或者 * T,* S 方法集总是包含 T +* T 方法
*/
三、面向对象
go 没有 class 关键字,struct 替代了 class 的作用,go 对于面向对象的支持是不一样的。拿 struct 来说,struct 没有继承的概念,go 语言是通过 组合的概念来进行面向对象编程。面向对象的目的其实就是代码复用,go 通过组合不同的结构体,使这个 struct 具有更多的功能,复用其他结构体的属性和方法,这个就是 struct 的继承。
所以,对 go 语言来说,封装采用首字母大小写的可见性支持,继承采用不同 struct 的组合来实现。多态会在讲接口的时候说。
1、封装
go 是直接支持 strut 的封装的,go 语言的可见性是根据首字母大小写来实现的。首字母大写表示对外部可见,等同 Java 中的 public,首字母小写,对外部不可见,等同于 Java 中的 private。
type Student struct{// 声名一个对包外部可见的结构体,
name string
age int
}
func (s *Student)setName(name string){
s.name=name
}
func (s *Student)setAge(age int){
s.age=age
}
func (s *Student)getName() string{
return s.name
}
func (s *Student)getAge()int{
return s.age
}
func main(){
s:=Student{}
fmt.Println(s)
s.setName(“ 张三 ”)
s.setAge(18)
fmt.Println(s)
}
// 输出
{0}
{张三 18}
这里的可见不可见是相对于包外部说得,对于同一个包内都是可见的。
2、继承
使用匿名字段或者叫结构体嵌入,可以变向的实现继承,并且可以实现多继承
go 语言的 struct 使用组合的方式实现继承,比传统的面向对象更加灵活,可以有效的避免类似 Java 的超多层级的继承。而 go 另辟蹊径,根据需要自己组合,也就是组合大于继承。代码见上面方法的匿名字段。