Golang结构体、“对象”的用法

1
2
3
4
type User struct {
name string
age int
}

结构体方法

结构体方法本质还是一个函数,只是这个函数与结构体绑定。所以首字母大小写能决定包外能不能访问。

1
2
3
4
5
6
7
8
9
10
11
func (u *User) changeAge(age int) {
fmt.Printf("ptr: %p\n", u)
u.age = age
return
}

func (u User) changeName(name string) {
fmt.Printf("value: %p\n", &u)
u.name = name
return
}

调用

1
2
3
4
5
6
7
8
9
10
11
12
u1 := &User{
name: "Mitaka",
age: 18,
}
u2 := User{
name: "xiaoyeshiyu",
age: 28,
}
u2.changeAge(18)
u1.changeName("xiaoyeshiyu")
fmt.Printf("u1: %+v\n", u1) // u1: &{name:Mitaka age:18}
fmt.Printf("u2: %+v\n", u2) // u2: {name:xiaoyeshiyu age:18}

可以看到,即使是值类型,也可以调用引用传递方法,而且是传递变量的指针,引用传递。同样的,即使是指针类型,也可以调用值传递方法,传递的是拷贝后的变量。

结构体判断和转换

1
2
3
4
5
6
7
8
9
10
11
12
13
type A struct {
x int
y int
}

type B struct {
x int
y int
}

var a A
var b B
fmt.Println(a == b) // 编译器报错:invalid operation: a == b (mismatched types A and B)

说明结构体即使字段类型一样,也不能比较。

1
2
c := B(a)
fmt.Println(c == b)

结构体和其他类型进行转换时,需要有完全相同的字段(名字、个数和类型)。顺序不同也不能转换。

1
2
3
4
5
6
7
8
9
type A struct {
y int
x int
}

type B struct {
x int
y int
}

即使类型重新定义也不能转换

1
2
3
4
5
type A struct {
y int
x int
}
type C A
1
2
3
4
5
6
7
8
var a A
var c C
//fmt.Println(a == c) 报错:invalid operation: a == c (mismatched types A and C)
a = A(c)
// a = C(a) 报错:cannot use C(a) (value of type C) as type A in assignment
//fmt.Println(a == c) 报错:invalid operation: a == c (mismatched types A and C)
a1 := C(a)
fmt.Println(c == a1)

方法表达式

将对象的方法函数提取出来,后续执行

1
2
3
4
f1 := u2.changeAge
f1(18) // 隐式传递接受者 u2
f2 := u1.changeName
f2("xiaoyeshiyu")

或者通过类型提取方法,后续传参执行

1
2
3
4
f1 := (*User).changeAge
f1(&u2, 18) // 显示传递接受者 &u2
f2 := User.changeName
f2(*u1, "xiaoyeshiyu")

此时传参需要严格按照函数定义,传递指针类型还是值类型。

方法集

每个类型都有与之关联的方法集,这会影响到接口实现规则。

  • 类型T 方法集包含全部receiver T 方法
  • 类型 *T 方法集包含全部 receiver T + *T 方法
  • 如类型S包含匿名字段T,则S*S方法集包含T方法
  • 如类型S包含匿名字段 *T,则s*S方法集包含T + *T 方法
  • 不管嵌入T*T*S方法集总是包含T + *T 方法

所以为什么实现了T方法集,却并不代表实现了全部receiver *T 方法,这是因为方法集合仅用于判断某类型是否实现某接口类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
type T struct {
a int
}

func (t T) M1() {
t.a = 10
}

func (t *T) M2() {
t.a = 11
}

func main() {
var t1 T
t1.M1()
t1.M2()

var t2 = &T{}
t2.M1()
t2.M2()
}

通过类型实例来调用方法时,Go会提供“语法糖”。上面这个例子先声明了类型T的变量t1,我们看到它不仅可以调用其方法集合中receiver参数类型为T的方法M1,它还可以直接调用不属于其方法集合的、receiver参数类型为T的方法M2。T类型的实例t1之所以可以调用receiver参数类型为T的方法M2都是Go编译器在背后自动进行转换的结果,即t1.M2()这种用法是Go提供的“语法糖”:Go判断t1的类型为T,与方法M2的receiver参数类型*T不一致后,会自动将t1.M2()转换为(&t1).M2()。

同理,类型为T的实例t2,它不仅可以调用receiver参数类型为T的方法M2,还可以调用receiver参数类型为T的方法M1,这同样是因为Go编译器在背后做了转换:Go判断t2的类型为*T,与方法M1的receiver参数类型T不一致后,会自动将t2.M1()转换为(*t2).M1()。

表达式原理

结构体方法引用的三种方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
type Data struct{}

func (Data) TestValue() {}

func (*Data) TestPointer() {}

func main() {
var v Data
v.TestValue()
v.TestPointer()

var p *Data
//p.TestValue() // invalid memory address or nil pointer dereference 此时调用会拷贝p
p.TestPointer()

Data{}.TestValue()
//Data{}.TestPointer() // cannot call pointer method TestPointer on Data 此时调用会引用底层指针

//(*Data).TestValue(nil) // panic: value method main.Data.TestValue called using nil *Data pointer 此时调用会拷贝对象
(*Data).TestPointer(nil) // 这里不会报错,nil也是一个合法的接收器类型

Data.TestValue(Data{})
//Data.TestPointer(Data{}) cannot call pointer method TestPointer on Data 此时调用会引用底层指针
}

面向对象

Golang面向对象编程建立在对象的方法上,实现了封装和继承。

封装

指不需要外部了解内部结构,通过对外暴露的方法实现功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type class struct {
a int
}

func NewClass(a int) class {
return class{a: a}
}

func (c *class) Get() int {
return c.a
}

func (c *class) Set(a int) {
c.a = a
}

除了构造函数,和暴露出去的函数,其他都小写。

调用时,只能通过共有函数设置、修改变量,而不能通过对象本身直接设置。

继承

集成多用于结构体嵌套的情况下,结构体可以调用子结构体的方法

1
2
3
4
5
6
7
8
9
10
type Animal struct {
}

type Cat struct {
Animal
}

func (a Animal) Sing() {
fmt.Println("sing")
}

匿名结构体嵌套

当结构体嵌套,出现匿名结构体,匿名结构体中成员有同名变量,或者父结构体与子结构体有同名变量,或者同名方法。此时调用变量或者方法的标准是:简洁、准确。

简洁:默认情况下调用父结构体

准确:当需要调用子结构体,则制定子结构体名称

同类型匿名结构体只能有一个,

1
2
3
4
5
6
7
8
9
type Cat struct {
Animal
Animal // 重复字段 'Animal'
}

type Cat struct {
int // 重复字段 'int'
int
}

重写

Golang作为一种强类型语言,不支持重写。也就是说,父类子类有相同方法,子类通过匿名成员执行父类方法时,如果此时调用父类子类同名方法,会执行父类自身的方法,这个点跟Java不一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package main

import "fmt"

type Parent struct {
}

func (p Parent) Say() {
fmt.Println("I am ", p.name())
}

func (p Parent) name() string {
return "parent"
}

type Chiled struct {
Parent
}

func (c Chiled) name() string {
return "chiled"
}

func main() {
var c Chiled
c.Say()
}

输出

1
I am  parent