在Go语言中,Go方法是作用在接收者(receiver)上的一个函数,接收者是某种类型的变量。所以,在Go语言中,方法是一种特殊类型的函数。
接收者可以是任意类型(接口、指针除外),包括结构体类型、函数类型,可以是int、bool、string或数组别名类型。接收者不能是一个接口类型,因为接口是一个抽象定义,但是方法却必须是具体的实现。
接收者也不能是一个指针类型,但需要注意的是它可以是任何其他允许类型的指针。一个类型加上它的方法就像面向对象中的一个类,不同的是,在Go语言中,类型的代码和与它相关的方法代码可以存在于不同的源文件中,当然它们必须在同一个包中。
1、方法的声明
方法是函数,所以不允许方法的重载,对于一个类型只能有一个给定名称的方法。
语法:func (recv receiver_type) methodName(parameter_list) (return_value_list){...}
- 在方法名之前,func关键字之后的括号中指定接收者。
- 如果recv是接收者的一个实例,Method1是接收者类型的一个方法名,那么方法调用遵循传统的选择器符号:recv.Method1()。
- 如果recv是一个指针,Go语言会自动解析该引用值,如果调用的方法不需要使用recv的值,可以用"_"符号替换:
func (_receiver_type) methodName(parameter_list) (return_value_list){...}
recv与面向对象语言中的this或self等类似,但recv并不是一个关键字,Go语言中也没有this和self这两个关键字,所以,也可以使用this或self作为接收者的实例化的名字:
type TwoInts struct {
a int
b int
}
func (tn *TwoInts) AddThem() int {
return tn.a + tn.b
}
func (tn *TwoInts) AddToParam(param int) int {
return tn.a + tn.b + param
}
func main() {
two1 := TwoInts{5, 10}
fmt.Printf("和为:%d\n", two1.AddThem())
fmt.Printf("将它们添加到参数:%d\n", two1.AddToParam(5))
}
函数和方法的区别如下:
- 函数将变量作为参数:Function(recv);方法在变量上被调用:recv.Method1()。
- 当接收者是指针时,方法可以改变接收者的值或状态,这一点函数也可以做到(当参数作为指针传递,即通过引用调用时,函数也可以改变参数的状态)。
- 接收者必须有一个显式的名字,这个名字必须在方法中被使用。receiver_type称为"接收者类型",这个类型必须在和方法同样的包中被声明。
- 在Go语言中,"接收者类型"对应的方法不应该写在类型结构中,就像面向对象语言的类那样,降低耦合性,类型和方法之间的关联由接收者来建立。方法与结构体没有混在一起,独立之后更容易维护,使他人更容易理解。
2、结构体中的方法
面向过程中没有"方法"的概念,只能通过结构体和函数,由使用者使用函数参数和调用关系来形成接近"方法"的概念:
type Bag struct {
items []int
}
// 模拟将物品放入背包的过程
func Insert(b *Bag, itemId int) {
b.items = append(b.items, itemId)
}
func main() {
bag := new(Bag)
Insert(bag, 001)
fmt.Println("-----%T----")
fmt.Printf("%T\n", bag) // *main.Bag
fmt.Printf("%T\n", *bag) // main.Bag
fmt.Printf("%T\n", &bag) //**main.Bag
fmt.Println("-----%v----")
fmt.Printf("%v\n", bag) // &{[1]}
fmt.Printf("%v\n", *bag) // {[1]}
fmt.Printf("%v\n", &bag) // 0xc000050020
}
使用Go语言的结构体为*Bag创建一个方法:
type Bag struct {
items []int
}
// 模拟将物品放入背包的过程
func (b *Bag) Insert(itemId int) {
b.items = append(b.items, itemId)
}
func main() {
bag := new(Bag)
bag.Insert(001)
fmt.Printf("%T\n", *bag) // main.Bag
fmt.Printf("%v\n", *bag) // {[1]}
}
- Insert(itemid int)的写法与函数一致。(b *Bag)表示接收器,即Insert作用的对象实例。
- 在Insert()转换为方法后,就可以像其他语言一样,用面向对象的方法来调用bag的Insert。
使用type关键字可以定义出新的自定义类型。之后就可以为自定义类型添加各种方法。
// 将int定义为MyInt类型
type MyInt int
// 为MyInt添加IsZero()方法
func (m MyInt) IsZero() bool {
return m == 0
}
// 为MyInt添加Add()方法
func (m MyInt) Add(other int) int {
return other + int(m)
}
func main() {
var b MyInt
fmt.Println(b.IsZero()) // true
b = 1
fmt.Println(b.Add(2)) // 3
}
- MyInt类型添加IsZero()方法。该方法使用(m MyInt)的非指针接收器。数值类型没有必要使用指针接收器。
- 由于m的类型是MyInt类型,但其本身是int类型,因此可以将m从MyInt类型转换为int类型再进行计算。
- 调用b的IsZero()方法。由于使用非指针接收器,b的值会被复制进入IsZero()方法进行判断。
- 调用b的Add()方法。同样也是非指针接收器,结果直接通过Add()方法返回。
Go语言中的大多数类型都是值语义,并且都可以包含对应的操作方法。在需要的时候,可以给任何类型(包括内置类型)"增加"新方法。而在实现某个接口时,无须从该接口继承(事实上,Go语言根本就不支持面向对象思想中的继承语法),只需要实现该接口要求的所有方法即可。任何类型都可以被Any类型引用,Any类型就是空接口,即interface{}。
type Integer int
func (a Integer) Less(b Integer) bool {
return a < b
}
func main() {
var a Integer = 1
if a.Less(2) {
fmt.Println(a, "Less 2") // 1 Less 2
}
}
- 定义了一个新类型Integer,它和int没有本质上的不同,只是它为内置的int类型增加了一个新方法Less()。这样实现了Integer后,就可以让整型像普通的类一样使用。
3、工厂方法创建结构体
在面向对象编程中,可以通过构造子方法实现工厂模式(一般是new Object等),但在Go语言中并不能这样构造子方法,而是相应地提供了其他方案。
以结构体为例,通常会为结构体类型定义一个工厂,按惯例,工厂的名字以new或New开头。假设定义了如下File结构体类型:
//不强制使用构造函数,首字母大写
type File struct {
fd int // 文件描述符
name string // 文件名
}
// 结构体类型对应的工厂方法,它返回一个指向结构体实例的指针:
func NewFile(fd int, name string) *File {
if fd < 0 {
return nil
}
return &File{fd, name}
}
- 调用:
f := NewFile(10, "./test.txt")
- 如果File是一个结构体类型,那么表达式new(File)和&File{}是等价的,这可以和大多数面向对象编程语言中笨拙的初始化方式做个比较:File f = new File(...)。
- 可以说工厂实例化了类型的一个对象,就像在基于类的面向对象语言中那样。如果想知道结构体类型T的一个实例占用了多少内存,可以使用:size := unsafe.Sizeof(T{})。
4、基于指针对象的方法
现在知道了Go语言不支持类似其他面向对象语言的传统类,相反,Go语言使用结构体替代。Go语言支持在结构体类型上定义方法,这一点非常类似于其他面向对象语言中的传统类方法。
在结构体类型上可以定义两种方法,分别基于指针接收器和基于值接收器。值接收器意味着复制整个值到内存中,内存开销非常大,而基于指针的接收器仅仅需要一个指针大小的内存。
因此,性能决定了哪种方法更值得推崇,recv最常见的是一个指向receiver_type的指针(因为不需要复制整个实例,若是按值调用就会复制整个实例),特别是在接收者类型是结构体时,性能优势就更突出了。
如果想要方法改变接收者的数据,就在接收者的指针类型上定义该方法,否则,就在普通的值类型上定义方法:
type HttpResponse struct {
status_code int
}
func (r *HttpResponse) validResponse() {
r.status_code = 200
}
func (r HttpResponse) updateStatus() string {
return fmt.Sprint(r)
}
func main() {
var r1 HttpResponse // r1 是值
r1.validResponse()
fmt.Println(r1.updateStatus()) // {200}
r2 := new(HttpResponse) // r2 是指针
r2.validResponse()
fmt.Println(r2.updateStatus()) // {200}
}
- validResponse()接收一个指向HttpResponse的指针,并改变它内部的成员;
- updateStatus()复制HttpRespose的值并只输出HttpResponse的内容,r1是值而r2是指针。
接着,在updateStatus()中改变接收者r的值,将会看到它可以正常编译,但是开始的r值没有被改变。因为指针作为接收者不是必需的。例如,Point的值仅仅用于计算:
type Point struct {
x, y, z float64
}
func (p Point) Abs() float64 {
return math.Sqrt(p.x*p.x + p.y*p.y + p.z*p.z)
}
func main() {
// 可以把p定义为一个指针来减少内存占用:
p := &Point{3, 4, 5}
fmt.Println(p.Abs()) // 7.0710678118654755
}
- 上面的写法内存占用稍微有点高,因为Point是作为值传递给方法的,因此传递的是它的副本,这在Go语言中是合法的。
5、嵌入类型的方法
由于Go语言并不是一门传统意义上的面向对象编程语言(Java、PHP等),所以,Go语言无法在语言层面上直接实现类的继承,但由于Go语言提供了创建匿名结构体的方法,所以,可以把匿名结构体嵌入有名字的结构体内部,这样有名字的结构体也会拥有其内部匿名结构体的那些方法,在效果上等同于面向对象编程中的类的继承。这与Python、Ruby等语言中的混入(mixin)相似:
type Point struct {
x, y float64
}
func (p *Point) Abs() float64 {
return math.Sqrt(p.x*p.x + p.y*p.y)
}
// NamePoint 结构体内部包含匿名字段Point
type NamedPoint struct {
Point
name string
}
func main() {
n := &NamedPoint{Point{3, 4}, "Python"}
fmt.Println(n.Abs()) // -> 5
}
将一个已存在类型的字段和方法注入另一个类型中即为内嵌,匿名字段上的方法"晋升"成为了外层类型的方法。当然类型可以有只作用于本身实例而不作用于内嵌"父"类型上的方法,可以覆写方法(像字段一样)。
和内嵌类型方法具有相同名字的外层类型的方法,会覆写内嵌类型对应的方法
func (n *NamedPoint) Abs() float64 {
return n.Point.Abs() * 100
}
func main() {
n := &NamedPoint{Point{3, 4}, "Python"}
fmt.Println(n.Abs()) //此时调用的Abs是NamedPoint类型的 -> 500
}
- 一个结构体可以嵌入多个匿名类型,可以有一个简单版本的多重继承:typeChild struct { Father; Mother}。
- 结构体内嵌和自己在同一个包中的结构体时,可以彼此访问对方所有的字段和方法。
6、多重继承
多重继承在生活中经常遇见,例如,孩子继承父母的特征,父母是两个父级类。在大部分面向对象语言中,是不允许多重继承的,因为这会导致编译器变得复杂,不过由于Go语言并没有类的概念,所谓继承其实是内嵌结构体,通过在类型中嵌入所有必要的父类型,可以很简单地实现多重继承。Go语言的多重继承不支持多重嵌套(即父级类型内部不允许有匿名结构体字段)。
假设有一个类型CameraPhone,通过它可以调用Call()函数,也可以调用TakeAPicture()函数,但是第一个方法属于类型Phone,第二个方法属于类型Camera。
只要嵌入这两个类型就可以解决这个问题,代码如下:
type Camera struct {
}
func (C *Camera) TakeAPicture() string {
return "拍照"
}
type Phone struct{}
func (p *Phone) Call() string {
return "响铃"
}
type CameraPhone struct {
Camera
Phone
}
func main() {
cp := new(CameraPhone)
fmt.Println("新款拍照手机有多款功能:")
fmt.Println("打开相机:", cp.TakeAPicture())
fmt.Println("电话来电:", cp.Call())
}