在面向对象编程语言中,我们可以使用类(class
)来模拟现实世界的实体,通过类的属性与方法,我们可以扩展自己想要的类型。
Go
语言中并没有类的概念,不过Go
支持定义方法(method
),Go的方法不是定义在类中的,那Go的方法定义在哪里的呢?
在这篇文章中我们就来探讨一下!
自定义数据类型
要讲清楚Go的方法,先了解Go的自定义数据类型。
Go
作为一个数据类型系统,内置许多的基础数据类型供我们使用,比如int
,unit
,string
,map
,slice
等。
如果基础数据类型还不能满足我们的需求,或者我们想和面向对象编程语言一样,定义一个有多个属性与方法的数据实体,Go语言的结构体(struct
)可以达到类似的效果:
go
代码解读
复制代码
type Car struct{ ID int Band string Name string }
在Go
语言中,通过关键词type
定义的数据类型,称为自定义类型,其语法为:
bash
代码解读
复制代码
type 自定义类型名称 基础数据名称
显然,结构体就是一种自定义数据类型,当然,除了结构体,我们也可以在其他内置类型的基础上创建任何的数据类型:
go
代码解读
复制代码
type Reason int type Month int
定义好数据类型之后,就可以像使用内置数据类型一样,用自定义类型定义变量或常量了:
ini
代码解读
复制代码
package main func main(){ const( Spring Reason = 1 Summer Reason = 2 Autumn Reason = 3 Winter Reason = 4 ) const ( January Month = 1 + iota February March April May June July August September October November December ) }
方法的创建
Go语言的方法(method
)本质是什么?简单来说就是函数(func
)。
方法与函数的区别在于方法必须有一个自定义类型的接收器,在Go语言中,自定义数据类型可以通过方法来扩展功能。
方法的创建
方法本质上就是函数,所以其创建也与函数相似,只要在关键字func
与函数名
中间加上一个用小括号括起来的接收器即可,如下图所示:
代码示例:
go
代码解读
复制代码
type User struct{ ID int Name string } func (u User)Say(message string){ //... } func (u *User)Run(){ //... }
接收器的数据类型只能是使用type
创建的数据类型,Go内置的数据类型不能作为接收器:
kotlin
代码解读
复制代码
//报错,int,string等内置数据类型不能作为接收器 func (r int)String(){ if r == 1 { return "春天" } else if r == 2 { return "夏天" } else if r == 3 { return "秋天" } else { return "冬天" } }
同一个数据类型上不能两个相同名称的方法:
go
代码解读
复制代码
type Reason int func (r Reason) String() string { if r == 1 { return "春天" } else if r == 2 { return "夏天" } else if r == 3 { return "秋天" } else { return "冬天" } } //报错 func (r Reason) String() string { }
方法的调用
要调用方法,必须先创建对应自定义数据类型的变量,然后使用变量名后跟上一个点号来调用对应的方法:
go
代码解读
复制代码
package main import "fmt" type Reason int func (r Reason) String() string { if r == 1 { return "春天" } else if r == 2 { return "夏天" } else if r == 3 { return "秋天" } else { return "冬天" } } type User struct { ID int Name string } func (u User) Say(message string) { fmt.Println(message) } func main() { u := User{ID: 1, Name: "test"} //创建变量 u.Say("Hello World") //调用方法 var reason Reason = 1 fmt.Println(reason.String()) //输出:春天 }
方法的可见性
在面向对象编程语言中,如果不想一个方法被外部调用,可以将方法定义可见性定义为private
,这就是面向对象最重要特性之一:封装。
Go语言控制可见性是通过首字母是否大小写来实现的,方法名以大写字母开头的可在包外调用,方法名以小写字母开头,则只允许包内调用:
go
代码解读
复制代码
package cart type Cart struct { } func NewCart() *Cart { return &Cart{} } func (c *Cart) Lock() error { //... return nil } func (c *Cart) TotalPrice() (int, error) { //... return 0, nil } func (c *Cart) delete() error { //... return nil }
在main
包中调用:
go
代码解读
复制代码
package main import ( "app/cart" "fmt" "log" ) func main() { myCart := cart.NewCart() totalPrice, err := myCart.TotalPrice() if err != nil { log.Printf("impossible to compute price of the cart: %s", err) return } fmt.Printf("TotalPrice:%d\n", totalPrice) //错误,该方法不可见 //myCart.delete() }
接收器
接收器可以看作是方法的一个参数,但不在方法的形参列表中,而是写在方法名前面,一个方法只能有一个接收器,当通过自定义类型的变量调用方法时,Go会将调用者复制给接收器。
go
代码解读
复制代码
type User struct{ ID int FirstName string LastName string } func (u User) GetFirstName(){ return u.FirstName //通过接收器访问当前接收器的字段 }
值接收器和指针接收器
方法的接收器有两种:值接收器和指针接收器。
前面我们的很多示例都是使用值接收器:
go
代码解读
复制代码
func (u User) GetLastName(){ return u.FirstName //通过接收器访问当前接收器的字段 }
指针接收器的写法就是在自定义类型前面加一个*号表示指向该类型的指针:
go
代码解读
复制代码
func (u *User) GetFirstName(){ return u.FirstName //通过接收器访问当前接收器的字段 }
值接收器与指针接收器有什么区别呢?
当通过类型变量调用方法时,会把调用者复制给接收器,无论是值接收器还是指针接收器,都会发生复制,所不同的是,使用值接收器时,会把调用者的值复制给接收器,使用指针接收器时,会把调用者的内存地址复制给接收器。
因此使用指针接收器有两个好处:
- 当调用者变量本身数据比较大时,指针接收器可以避免大数据复制。
- 指针接收器与调用者变量指向同一个内存地址,因此可以通过指针接收器修改调用者本身,这点值接收器是无法做到的。
下面我们通过一个示例来演示一下:
go
代码解读
复制代码
package main import ( "fmt" "strconv" ) type Student struct { ID int Name string } type ClassRoom struct { ID string Name string Students []Student } func (c ClassRoom) ChangeName1(name string) { fmt.Printf("值接收器的内存地址:%p\n", &c) c.Name = name } func (c *ClassRoom) ChangeName2(name string) { fmt.Printf("指针接收器的内存地址:%p\n", c) c.Name = name } func main() { var students []Student for i := 1; i <= 100; i++ { students = append(students, Student{ID: i, Name: "同学" + strconv.Itoa(i)}) } classRoom := ClassRoom{ID: "001", Name: "高中一班", Students: students} fmt.Printf("调用者本身的内存地址:%p\n", &classRoom) classRoom.ChangeName1("高中二班") fmt.Println(classRoom.Name) //输出:高中一班 classRoom.ChangeName2("高中二班") fmt.Println(classRoom.Name) //输出:高中二班 }
在这个示例程序中,我们创建一个ClassRoom
类型的变量表示一个教室,该教室包含100
个学生(Student
)的信息,ChangeName1()
方法使用的是值接收器,ChangeName2()
方法使用的是指针接收器。
上面的示例运行结果为:
代码解读
复制代码
调用者本身的内存地址:0xc00005c040 值接收器的内存地址:0xc00005c080 高中一班 指针接收器的内存地址:0xc00005c040 高中二班
通过运行结果我们可以发现,使用指针接收器,接收器与调用指向同一个内存地址,这样可以修改调用者自身的属性,也可以避免大量数据的复制。
接收器的命名惯例
指针接收器的作用类似面向对象编程类的this
,用于引用对象自身,不过Go
并不推荐将接收器命名为this
,而是推荐使用接收器类型的首字母小写:
go
代码解读
复制代码
type Reason int //不推荐 func (this Reason)String()string{ } type Car struct{ ID int Name string } //推荐 func (c Car)Run(){ }
小结
与其他面向对象编程语言不同,Go的方法并不是定义在类中,而是附加于自定义类型之上的,可以更加灵活地扩展自定义数据类型的功能与行为。
最后,总结一下,阅读完这篇文章后应该掌握的几个知识点:
- 自定义类型是什么,如何自定义数据类型
- 方法是什么,如何创建与调用方法。
- 接收器是什么?什么是指针接收器,什么是值接收器。
- 什么情况下要用指针接收器。