Golang Pearl 01

在Golang中,面向对象的三大特性(封装、继承、多态)只有封装这一特性被实现,但是在Go中封装的形式不像常规的面向对象语言那样,Go采用了一种比较独特的封装方式,这种封装方式被称为方法集的概念。

一言以蔽之,就是指针比实体大

具体的规则是这样:

  1. 类型T方法集包含全部receiver T方法
  2. 类型* T方法包含全部receiver T和* T方法
  3. 如果类型S包含类型T,则S方法集包含T方法
  4. 如果类型S包含类型* T,则S方法集包含T和* T方法
  5. 无论* S包含T合适* T,* S方法集都包含T和* T方法

普通函数

让我们从简单的测试开始,从这个例子我们可以看出对于函数的receiver是User(而不是* User)的函数,无论使用User还是* User都可以正常调用,Go不会编译错误。

type User struct {
	id   int
	name string
}

func (self User) Test() {
	fmt.Printf("%p, %v\n", &self, self)
}

func main() {
	u := User{1, "Tom"}
	fmt.Printf("%p, %v\n", &u, u)

	u.Test()
	(&u).Test()
}

从运行的结果我们可以得知,三次打印的对象地址是不同的,也就是说如果函数的receiver是User而不是指针类型,那么receiver一定是copy的,如果在函数中修改了对象的属性,原来的对象不会被更改。

作为验证,将函数的receiver改为指针类型:

type User struct {
	id   int
	name string
}

func (self *User) Test() {
	fmt.Printf("%p, %v\n", self, self)
}

func main() {
	u := User{1, "Tom"}
	fmt.Printf("%p, %v\n", &u, u)

	u.Test()
	(&u).Test()
}

可以看到三次打印的对象地址都是一个,也就是说,对象没有被拷贝。指针类型的变量调用实体类型的函数,并不妨碍实体类型函数的变量拷贝特性。

从以上两个例子可以看出User和* User类型的函数是可以相互集成的。

方法集

在方法集中,如果User类型实现了一个接口(Stringer),那么无论是传入User还是* User都是可以的:

type Stringer interface {
	String()
}

func Greet(stringer Stringer) {
	stringer.String()
}

type User struct {
	id   int
	name string
}

func (u User) String() {
	fmt.Println("hello", u)
}

func main() {
	u := User{1, "Tom"}
	fmt.Printf("%p, %v\n", &u, u)
	Greet(u)
	Greet(&u)
}

但是,如果是* User实现了这一接口,传入User是会出错的,这印证了开篇那句话,指针类型大于实体类型。

包含方法集

我们已经知道了类型直接实现接口的特性,那么如果类型A被类型B包含,前类型A又实现了一个接口,那么类型B会是什么情况呢?

type Stringer interface {
	String()
}

func Greet(stringer Stringer) {
	stringer.String()
}

type User struct {
	id   int
	name string
}

type SuperUser struct {
	User
	Sid int
}

func (u User) String() {
	fmt.Println("hello", u)
}

func main() {
	m := User{1, "Tom"}
	u := SuperUser{m, 1}

	fmt.Printf("%p, %v\n", &u, u)
	Greet(u)
	Greet(&u)
}

可以看到无论是SuperUser还是* SuperUser都因为包含了User而实现了Stringer这个接口。

如果SuperUser包含的是* User,效果是一样的。

type Stringer interface {
	String()
}

func Greet(stringer Stringer) {
	stringer.String()
}

type User struct {
	id   int
	name string
}

type SuperUser struct {
	*User
	Sid int
}

func (u *User) String() {       //或者(u User)亦可
	fmt.Println("hello", u)
}

func main() {
	m := User{1, "Tom"}
	u := SuperUser{&m, 1}

	fmt.Printf("%p, %v\n", &u, u)
	Greet(u)
	Greet(&u)
}

重写

如果SuperUser实现了这一接口,那么User实现接口的函数就被覆盖了。

type Stringer interface {
	String()
}

func Greet(stringer Stringer) {
	stringer.String()
}

type User struct {
	id   int
	name string
}

type SuperUser struct {
	*User
	Sid int
}

func (u User) String() {
	fmt.Println("hello,", u)
}

func (su SuperUser) String() {
	fmt.Println("Greeting,", su)
}

func main() {
	m := User{1, "Tom"}
	u := SuperUser{&m, 1}

	fmt.Printf("%p, %v\n", &u, u)
	Greet(u)
	Greet(&u)
}

从打印来看,可以知道调用的是SuperUser所实现的接口函数。