在Go语言中,Go方法是作用在接收者(receiver)上的一个函数,接收者是某种类型的变量。所以,在Go语言中,方法是一种特殊类型的函数。
接收者可以是任意类型(接口、指针除外),包括结构体类型、函数类型,可以是int、bool、string或数组别名类型。接收者不能是一个接口类型,因为接口是一个抽象定义,但是方法却必须是具体的实现。
接收者也不能是一个指针类型,但需要注意的是它可以是任何其他允许类型的指针。一个类型加上它的方法就像面向对象中的一个类,不同的是,在Go语言中,类型的代码和与它相关的方法代码可以存在于不同的源文件中,当然它们必须在同一个包中。
方法是函数,所以不允许方法的重载,对于一个类型只能有一个给定名称的方法。
语法:func (recv receiver_type) methodName(parameter_list) (return_value_list){…}
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))
}
函数和方法的区别如下:
面向过程中没有“方法”的概念,只能通过结构体和函数,由使用者使用函数参数和调用关系来形成接近“方法”的概念:
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]}
}
使用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
}
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
}
}
在面向对象编程中,可以通过构造子方法实现工厂模式(一般是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")
现在知道了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}
}
接着,在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
}
由于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
}
多重继承在生活中经常遇见,例如,孩子继承父母的特征,父母是两个父级类。在大部分面向对象语言中,是不允许多重继承的,因为这会导致编译器变得复杂,不过由于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())
}