在面向对象编程语言中,我们可以使用类(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 自定义类型名称 基础数据名称
显然,结构体就是一种自定义数据类型,当然,除了结构体,我们也可以在其他内置类型的基础上创建任何的数据类型:
代码语言:javascript复制go 代码解读复制代码type Reason int
type Month int
定义好数据类型之后,就可以像使用内置数据类型一样,用自定义类型定义变量或常量了:
代码语言:javascript复制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
与函数名
中间加上一个用小括号括起来的接收器即可,如下图所示:
代码示例:
代码语言:javascript复制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 "冬天"
}
}
同一个数据类型上不能两个相同名称的方法:
代码语言:javascript复制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 {
}
方法的调用
要调用方法,必须先创建对应自定义数据类型的变量,然后使用变量名后跟上一个点号来调用对应的方法:
代码语言:javascript复制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语言控制可见性是通过首字母是否大小写来实现的,方法名以大写字母开头的可在包外调用,方法名以小写字母开头,则只允许包内调用:
代码语言:javascript复制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:%dn", totalPrice)
//错误,该方法不可见
//myCart.delete()
}
接收器
接收器可以看作是方法的一个参数,但不在方法的形参列表中,而是写在方法名前面,一个方法只能有一个接收器,当通过自定义类型的变量调用方法时,Go会将调用者复制给接收器。
代码语言:javascript复制go 代码解读复制代码type User struct{
ID int
FirstName string
LastName string
}
func (u User) GetFirstName(){
return u.FirstName //通过接收器访问当前接收器的字段
}
值接收器和指针接收器
方法的接收器有两种:值接收器和指针接收器。
前面我们的很多示例都是使用值接收器:
代码语言:javascript复制go 代码解读复制代码func (u User) GetLastName(){
return u.FirstName //通过接收器访问当前接收器的字段
}
指针接收器的写法就是在自定义类型前面加一个*号表示指向该类型的指针:
代码语言:javascript复制go 代码解读复制代码func (u *User) GetFirstName(){
return u.FirstName //通过接收器访问当前接收器的字段
}
值接收器与指针接收器有什么区别呢?
当通过类型变量调用方法时,会把调用者复制给接收器,无论是值接收器还是指针接收器,都会发生复制,所不同的是,使用值接收器时,会把调用者的值复制给接收器,使用指针接收器时,会把调用者的内存地址复制给接收器。
因此使用指针接收器有两个好处:
- 当调用者变量本身数据比较大时,指针接收器可以避免大数据复制。
- 指针接收器与调用者变量指向同一个内存地址,因此可以通过指针接收器修改调用者本身,这点值接收器是无法做到的。
下面我们通过一个示例来演示一下:
代码语言:javascript复制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("值接收器的内存地址:%pn", &c)
c.Name = name
}
func (c *ClassRoom) ChangeName2(name string) {
fmt.Printf("指针接收器的内存地址:%pn", 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("调用者本身的内存地址:%pn", &classRoom)
classRoom.ChangeName1("高中二班")
fmt.Println(classRoom.Name) //输出:高中一班
classRoom.ChangeName2("高中二班")
fmt.Println(classRoom.Name) //输出:高中二班
}
在这个示例程序中,我们创建一个ClassRoom
类型的变量表示一个教室,该教室包含100
个学生(Student
)的信息,ChangeName1()
方法使用的是值接收器,ChangeName2()
方法使用的是指针接收器。
上面的示例运行结果为:
代码语言:javascript复制 代码解读复制代码调用者本身的内存地址: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的方法并不是定义在类中,而是附加于自定义类型之上的,可以更加灵活地扩展自定义数据类型的功能与行为。
最后,总结一下,阅读完这篇文章后应该掌握的几个知识点:
- 自定义类型是什么,如何自定义数据类型
- 方法是什么,如何创建与调用方法。
- 接收器是什么?什么是指针接收器,什么是值接收器。
- 什么情况下要用指针接收器。