# 10.6 方法 ## 10.6.1 方法是什么 在Go中,结构体就像是类的一种简化形式,那么OO程序员可能会问:类的方法在哪里呢?在Go中有一个概念,它和方法有着同样的名字,并且大体上意思相同:Go方法是作用在接收者(receiver)上的一个函数,接收者是某种类型的变量。因此方法是一种特殊类型的函数。 接收者类型可以是(几乎)任何类型,不仅仅是结构体类型:任何类型都可以有方法,甚至可以是函数类型,可以是int、bool、string或数组的alias类型。但是接收者不能是一个接口类型(参考 第11章),因为接口是一个抽象定义,但是方法却是具体实现;如果这样做会引发一个编译错误:*invalid receiver type…* 最后接收者不能是一个指针类型,但是它可以是任何其他允许类型的指针。 一个类型加上它的方法等价于OO中的一个类。一个重要的区别是:在Go中,类型的代码和绑定在它上面的方法的代码可以不放置在一起,它们可以存在在不同的源文件,唯一的要求是:它们必须是同一个包的。 类型T(或*T)上的所有方法的集合叫做类型T(或*T)的方法集。 因为方法是函数,所以同样的,不允许方法重载,即对于一个类型只能有一个给定名称的方法。但是如果基于接收者类型,是有重载的:具有同样名字的方法可以在2个或多个不同的接收者类型上存在,比如在同一个包里这么做是允许的: ```go func (a *denseMatrix) Add(b Matrix) Matrix func (a *sparseMatrix) Add(b Matrix) Matrix ``` alias类型不能有它原始类型上已经定义过的方法。 定义方法的一般格式如下: func (recv receiver_type) methodName(parameter_list) (return_value_list) { ... } 在方法名之前,func关键字之后的括号中指定receiver。 如果recv是receiver的实例,Method1是它的方法名,那么方法调用遵循传统的object.name选择器符号:**recv.Method1()** 如果recv一个指针,Go会自动解引用。 如果方法不需要使用recv的值,可以用*_*替换它,比如: func (_ receiver_type) methodName(parameter_list) (return_value_list) { ... } recv就像是OO语言中的this或self,但是Go中并没有这两个关键字。随个人喜好,你可以使用this或self作为receiver的名字。下面是一个结构体上的简单方法的例子: Listing 10.10—method .go ```go package main import "fmt" type TwoInts struct { a int b int } func main() { two1 := new(TwoInts) two1.a = 12 two1.b = 10 fmt.Printf("The sum is: %d\n", two1.AddThem()) fmt.Printf("Add them to the param: %d\n", two1.AddToParam(20)) two2 := TwoInts{3, 4} fmt.Printf("The sum is: %d\n", two2.AddThem()) } func (tn *TwoInts) AddThem() int { return tn.a + tn.b } func (tn *TwoInts) AddToParam(param int) int { return tn.a + tn.b + param } ``` 输出: The sum is: 22 Add them to the param: 42 The sum is: 7 下面是非结构体类型上方法的例子: Listing 10.11—method2.go: ```go package main import "fmt" type IntVector []int func (v IntVector) Sum() (s int) { for _, x := range v { s += x } return } func main() { fmt.Println(IntVector{1, 2, 3}.Sum()) // 输出是6 } ``` 练习 10.6:employee_salary.go 定义结构体employee,它有一个salary字段,给这个结构体定义一个方法giveRaise来按照指定的百分比增加薪水。 练习 10.7:iteration_list.go 下面这段代码有什么错? ```go package main import "container/list" func (p *list.List) Iter() { // ... } func main() { lst := new(list.List) for _= range list.Iter() { } } ``` 类型和作用在它上面定义的方法必须在同一个包里定义,这就是为什么不能在int、float或类似这些的类型上定义方法。试图在int类型上定义方法会得到一个编译错误: cannot define new methods on non-local type int 比如想在time.Time上定义如下方法: ```go func (t time.Time) first3Chars() string { return time.LocalTime().String()[0:3] } ``` 类型在在其他的,或是非本地的包里定义,在它上面定义方法都会得到和上面同样的错误。 但是有一个绕点的方式:可以先定义该类型(比如int,float)的别名类型,然后再为别名类型定义方法。或者像下面这样将它作为匿名类型嵌入在一个新的结构体中。当然方法只在这个别名类型上有效。 Listing 10.12—method_on_time.go: ```go package main import ( "fmt" "time" ) type myTime struct { time.Time //anonymous field } func (t myTime) first3Chars() string { return t.Time.String()[0:3] } func main() { m := myTime{time.Now()} // 调用匿名Time上的String方法 fmt.Println("Full time now:", m.String()) // 调用myTime.first3Chars fmt.Println("First 3 chars:", m.first3Chars()) } /* Output: Full time now: Mon Oct 24 15:34:54 Romance Daylight Time 2011 First 3 chars: Mon */ ``` ## 10.6.2 函数和方法的区别 函数将变量作为参数:*Function1(recv)* 方法在变量上被调用:*recv.Method1()* 在接收者是指针时,方法可以改变接收者的值(或状态),这点函数也可以做到(当参数作为指针传递,即通过引用调用时,函数也可以改变参数的状态)。 !!不要忘记Method1后边的括号(),否则会引发编译器错误:*method recv.Method1 is not an expression, must be called *!! 接收者必须有一个显式的名字,这个名字必须在方法中被使用。 *receiver_type*叫做*(接收者)基本类型*,这个类型必须在和方法同样的包中被声明。 在Go中,(接收者)类型关联的方法不写在类型结构里面,就像类那样;耦合更加宽松;类型和方法之间的关联由接收者来建立。 *方法没有和数据定义(结构体)混在一起:它们是正交的类型;表示(数据)和行为(方法)是独立的。* ## 10.6.3 指针或值作为接收者 鉴于性能的原因,recv最常见的是一个指向receiver_type的指针(因为我们不想要一个实例的拷贝,如果按值调用的话就会是这样),特别是在receiver类型是结构体时,就更这样了。 如果想要方法改变接收者的数据,就在接收者的指针类型上定义该方法。否则,就在普通的值类型上定义方法。 下面的例子pointer_value.go作了说明:change()接受一个指向B的指针,并改变它内部的成员;write()接受通过拷贝接受B的值并只输出B的内容。注意Go为我们做了探测工作,我们自己并没有指出是是否在指针上调用方法,Go替我们做了这些事情。b1是值而b2是指针,方法都支持运行了。 Listing 10.13—pointer_value.go: ```go package main import ( "fmt" ) type B struct { thing int } func (b *B) change() { b.thing = 1 } func (b B) write() string { return fmt.Sprint((b)) } func main() { var b1 B // b1是值 b1.change() fmt.Println(b1.write()) b2 := new(B) // b2是指针 b2.change() fmt.Println(b2.write()) } /* 输出: {1} {1} */ ``` 试着在write()中改变接收者b的值:将会看到它可以正常编译,但是开始的b没有被改变。 我们知道方法不需要指针作为接收者,如下面的例子,我们只是需要Point3的值来做计算: ```go type Point3 struct { x, y, z float } // A method on Point3 func (p Point3) Abs float { return math.Sqrt(p.x*p.x + p.y*p.y + p.z*p.z) } ``` 这样做稍微有点昂贵,因为Point3是作为值传递给方法的,因此传递的是它的拷贝,这在Go中合法的。也可以在指向这个类型的指针上调用此方法(会自动解引用)。 假设p3定义为一个指针:* p3 := &Point{ 3, 4, 5}* 可以这样写: * p3.Abs() 来替代 (*p3).Abs() * 像例子10.11(method1.go)中接收者类型是*TwoInts的方法AddThem(),它能在类型TwoInts的值上被调用,这是自动间接发生的。 因此two2.AddThem可以替代(&two2).AddThem()。 在值和指针上调用方法: 可以有连接到类型的方法,也可以有连接到类型指针的方法。 *但是这没关系:对于类型T,如果在*T上存在方法Meth(),并且t是这个类型的变量,那么t.Meth()会被自动转换为(&t).Meth().* *指针方法和值方法都可以在指针或非指针上被调用*,如下面程序所示,类型List在值上有一个方法Len(),在指针上有一个方法Append(),但是可以看到两个方法都可以在两种类型的变量上被调用。 Listing 10.14—methodset1.go: ```go package main import ( "fmt" ) type List []int func (l List) Len() int { return len(l) } func (l *List) Append(val int) { *l = append(*l, val) } func main() { // 值 var lst List lst.Append(1) fmt.Printf("%v (len: %d)", lst, lst.Len()) // [1] (len: 1) // 指针 plst := new(List) plst.Append(2) fmt.Printf("%v (len: %d)", plst, plst.Len()) // &[2] (len: 1) } ``` ## 10.6.4 方法和未导出字段 考虑person2.go中的person包:类型Person被明确的导出了,但是它的字段没有被导出。例如在use_person2.go中p.firsetname就是错误的。该如何在另一个程序中修改或者只是读取一个Person的名字呢? 这可以通过OO语言一个众所周知的技术来完成:提供getter和setter方法。对于setter方法使用Set前缀,对于getter方法只适用成员名。 Listing 10.15—person2.go: ```go package person type Person struct { firstName string lastName string } func (p *Person) FirstName() string { return p.firstName } func (p *Person) SetFirstName(newName string) { p.firstName = newName } ``` Listing 10.16—use_person2.go: ```go package main import ( "./person" "fmt" ) func main() { p := new(person.Person) // p.firstName undefined // (cannot refer to unexported field or method firstName) // p.firstName = "Eric" p.SetFirstName("Eric") fmt.Println(p.FirstName()) // Output: Eric } ``` *并发访问对象:* 对象的字段(属性)不应该由2个或2个以上的不同线程在同一时间去改变。如果在程序发生这种情况,为了安全并发访问,可以使用包sync(参考9.3)中的方法。在14.17我们会通过goroutines和channels探索另一种方式。 ## 10.6.5 内嵌类型的方法和继承 当一个匿名类型被内嵌在结构体中时,匿名类型的可见方法也同样被内嵌---在效果上等同于外层类型*继承*了这些方法:*将父类型放在子类型中来实现亚型*。这个机制提供了一种简单的方式来模拟经典OO语言中的子类和继承相关的效果,也类似Ruby中的混入(mixin)。 下面是一个示例(可以在练习 10.8中进一步学习):假定有一个Engine接口类型,一个Car结构体类型,它包含一个Engine类型的匿名字段: ```go type Engine interface { Start() Stop() } type Car struct { Engine } ``` 我们可以构建如下的代码: ```go func (c *Car) GoToWorkIn() { // get in car c.Start() // drive to work c.Stop() // get out of car } ``` 下面是method3.go的完整例子,它展示了内嵌结构体上的方法可以直接在外层类型的实例上调用: ```go package main import ( "fmt" "math" ) type Point struct { x, y float64 } func (p *Point) Abs() float64 { return math.Sqrt(p.x*p.x + p.y*p.y) } type NamedPoint struct { Point name string } func main() { n := &NamedPoint{Point{3, 4}, "Pythagoras"} fmt.Println(n.Abs()) // 打印5 } ``` 内嵌将一个已存在类型的字段和方法注入到了另一个类型里:匿名字段上的方法“晋升”成为了外层类型的方法。当然类型可以有只作用于本身实例而不作用于内嵌“父”类型上的方法, 可以覆写方法(像字段一样):和内嵌类型方法具有同样名字的外层类型的方法会覆写内嵌类型对应的方法。在Listing 10.18—method4.go中添加: ```go func (n *NamedPoint) Abs() float64 { return n.Point.Abs() * 100. } ``` 现在`fmt.Println(n.Abs())`会打印500. 因为一个结构体可以嵌入多个匿名类型,所以实际上我们可以有一个简单版本的多重继承,就像:`type Child struct { Father; Mother}`。在10.6.7中会进一步讨论这个问题。 结构体内嵌和自己在同一个包中的结构体时,可以彼此访问对方所有的字段和方法。 练习 10.8:inheritance_car.go 创建一个上面Car和Engine可运行的例子,并且给Car类型一个wheelCount字段和一个numberOfWheels()方法。 创建一个Mercedes类型,它内嵌Car,并新建Mercedes的一个实例,然后调用它的方法。 然后仅在Mercedes类型上创建方法sayHiToMerkel()并调用它。 ## 10.6.6 如何在类型中嵌入功能 主要有两种方法来实现在类型中嵌入功能: A. 聚合(或组合):包含一个所需功能类型的具名字段 B. 内嵌:内嵌(匿名地)所需功能类型,像前一节10.6.5 所示的那样 为了使这些概念具体化,假设有一个Customer类型,我们想让它通过Log类型来包含日志功能,Log类型只是简单地包含一个累积的消息(当然它可以是复杂的)。如果想让特定类型都具备日志功能,你可以实现一个这样的Log类型,然后将它作为特定类型的一个字段,并提供Log(),它返回这个日志的引用。 方式A可以通过如下方法实现(使用了10.7中的String()功能): Listing 10.19—embed_func1.go: ```go package main import ( "fmt" ) type Log struct { msg string } type Customer struct { Name string log *Log } func main() { c := new(Customer) c.Name = "Barak Obama" c.log = new(Log) c.log.msg = "1 - Yes we can!" // shorter c = &Customer{"Barak Obama", &Log{"1 - Yes we can!"}} // fmt.Println(c) &{Barak Obama 1 - Yes we can!} c.Log().Add("2 - After me the world will be a better place!") //fmt.Println(c.log) fmt.Println(c.Log()) } func (l *Log) Add(s string) { l.msg += "\n" + s } func (l *Log) String() string { return l.msg } func (c *Customer) Log() *Log { return c.log } ``` 输出: 1 - Yes we can! 2 - After me the world will be a better place! 相对的方式B可能会像这样: ```go package main import ( "fmt" ) type Log struct { msg string } type Customer struct { Name string Log } func main() { c := &Customer{"Barak Obama", Log{"1 - Yes we can!"}} c.Add("2 - After me the world will be a better place!") fmt.Println(c) } func (l *Log) Add(s string) { l.msg += "\n" + s } func (l *Log) String() string { return l.msg } func (c *Customer) String() string { return c.Name + "\nLog:" + fmt.Sprintln(c.Log) } ``` 输出: Barak Obama Log:{1 - Yes we can! 2 - After me the world will be a better place!} 内嵌的类型不需要指针,Customer也不需要Add方法,它使用Log的Add方法,Customer有自己的String方法,并且在它里面调用了Log的String方法。 如果内嵌类型嵌入了其他类型,也是可以的,那些类型的方法可以直接在外层类型中使用。 因此一个好的策略是创建一些小的、可复用的类型作为一个工具箱,用于组成域类型。 ## 10.6.7 多重继承 多重继承指的是类型获得多个父类型行为的能力,它在传统的面向对象语言中通常是不被实现的(C++和Python例外)。因为在类继承层次中,多重继承会给编译器引入额外的复杂度。但是Go语言中,通过在类型中嵌入所有必要的父类型,可以很简单的实现多重继承。 作为一个例子,假设有一个类型CameraPhone,通过它可以Call(),也可以TakeAPicture(),但是第一个方法属于类型Phone,第二个方法属于类型Camera。 只要嵌入这两个类型就可以解个问题,如下所示: ```go package main import ( "fmt" ) type Camera struct{} func (c *Camera) TakeAPicture() string { return "Click" } type Phone struct{} func (p *Phone) Call() string { return "Ring Ring" } type CameraPhone struct { Camera Phone } func main() { cp := new(CameraPhone) fmt.Println("Our new CameraPhone exhibits multiple behaviors...") fmt.Println("It exhibits behavior of a Camera: ", cp.TakeAPicture()) fmt.Println("It works like a Phone too: ", cp.Call()) } ``` 输出: Our new CameraPhone exhibits multiple behaviors... It exhibits behavior of a Camera: Click It works like a Phone too: Ring Ring 练习 10.9:point_methods.go: 从point.go开始(10.1的联系):使用方法来实现Abs()和Scale()函数,Point作为方法的接收者类型。也为Point3和Polar实现Abs()方法。做point.go中同样的事情,只是这次通过方法。 练习 10.10:inherit_methods.go: 定义一个结构体类型Base,它包含一个字段id,方法Id()返回id,方法SetId()修改id。结构体类型Person包含Base,及FirstName和LastName字段。结构体类型Employee包含一个Person和salary字段。 创建一个employee实例,然后显示它的id。 练习 10.11:magic.go: 首先预测一下下面程序的结果,然后动手实验下: ```go package main import ( "fmt" ) type Base struct{} func (Base) Magic() { fmt.Println("base magic") } func (self Base) MoreMagic() { self.Magic() self.Magic() } type Voodoo struct { Base } func (Voodoo) Magic() { fmt.Println("voodoo magic") } func main() { v := new(Voodoo) v.Magic() v.MoreMagic() } ``` ## 10.6.8 通用方法和方法命名 在编程中一些基本操作会一遍又一遍的出现,比如打开(Open)、关闭(Close)、读(Read)、写(Write)、排序(Sort)等等,并且它们都有一个大致的意思:打开(Open)可以作用于一个文件、一个网络连接、一个数据库连接等等。具体的实现可能千差万别,但是基本的概念是一致的。在Go语言中,通过使用接口(参考 第11章),标准库广泛的应用了这些规则,在标准库中这些通用方法都有一致的名字,比如Open()、Read()、Write()等。想写规范的Go程序,就应该遵守这些约定,给方法合适的名字和签名,就像那些通用方法那样。这样做会使Go开发的软件更加具有一致性和可读性。比如:如果需要一个convert-to-string方法,应该命名为String(),而不是ToString()(参考10.7). ## 10.6.9 和其他面向对象语言比较Go的类型和方法 在如C++、Java、C#和Ruby这样的面向对象语言中,方法在类的上下文中被定义和继承:在一个对象上调用方法时,运行时会检测类以及它的超类中是否有此方法的定义,如果没有会导致异常发生。 在Go中,这样的继承层次是完全没必要的:如果方法在此类型定义了,就可以调用它,和其他类型上是否存在这个方法没有关系。在这个意义上,Go具有更大的灵活性。 下面的模式就很好的说明了这个问题: ![](images/10.6.9_fig10.4.jpg?raw=true) Go不需要一个显式的类定义,如同Java、C++、C#等那样,相反地,,“类”是通过提供一组作用于一个共同类型的方法集来隐式定义的。类型可以是结构体或者任何用户自定义类型。 比如:我们想定义自己的Integer类型,并添加一些类似转换成字符串的方法,在Go中可以如下定义: ```go type Integer int func (i *Integer) String() string { return strconv.Itoa(i) } ``` 在Java或C#中,这个方法需要和类Integer的定义放在一起,在Ruby中可以直接在基本类型int上定义这个方法。 **总结:** 在Go中,类型就是类(数据和关联的方法)。Go不知道类似OO语言的类继承的概念。继承有两个好处:代码复用和多态。 在Go中,代码复用通过组合和委托实现,多态通过接口的使用来实现:有时这也叫*组件编程*。 许多开发者说相比于类继承,Go的接口提供了更强大、却更简单的多态行为。 **备注**: 如果真的需要更多OO的能力,看一下goop包(Go Object-Oriented Programming),它来自与Scott Pakin[(https://github.com/losalamos/goop]: 它给Go提供了JavaScript风格的对象(基于原型的对象),并且支持多重继承和类型独立分派,通过它可以实现你喜欢的其他编程语言里的一些结构。 问题 10.1: 我们在某个类型的变量上使用点号调用一个方法:variable.method(),在使用Go以前,在哪儿碰到过OO的点号? 问题 10.2: a) 假设定义: `type Integer int`,完成get()方法的方法体: `func (p Integer) get() int { ... }` b) 定义: `func f(i int) {}; var v Integer` , 如何就v作为参数调用f? c) 假设Integer定义为:`type Integer struct {n int}`,完成get()方法的方法体:`func (p Integer) get() int { ... }` d) 对于新定义的Integer,和b)中同样的问题 ## 链接 - [目录](directory.md) - 上一节:[10.5 匿名字段和内嵌结构体](10.5.md) - 下一节:[10.7 TODO](10.7.md)