Go 方法

在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())
}
相关推荐
Lizhihao_17 分钟前
JAVA-队列
java·开发语言
远望清一色35 分钟前
基于MATLAB边缘检测博文
开发语言·算法·matlab
何曾参静谧43 分钟前
「Py」Python基础篇 之 Python都可以做哪些自动化?
开发语言·python·自动化
Prejudices1 小时前
C++如何调用Python脚本
开发语言·c++·python
我狠狠地刷刷刷刷刷1 小时前
中文分词模拟器
开发语言·python·算法
wyh要好好学习1 小时前
C# WPF 记录DataGrid的表头顺序,下次打开界面时应用到表格中
开发语言·c#·wpf
AitTech1 小时前
C#实现:电脑系统信息的全面获取与监控
开发语言·c#
qing_0406031 小时前
C++——多态
开发语言·c++·多态
孙同学_1 小时前
【C++】—掌握STL vector 类:“Vector简介:动态数组的高效应用”
开发语言·c++
froginwe111 小时前
XML 编辑器:功能、选择与使用技巧
开发语言