- 方法声明
方法的声明和普通函数的声明类似,只是在函数名字前面多了一个参数,这个参数把这个方法绑定到这个参数对应的类型上
package geometry import ( "fmt" "math" ) type Point struct { X, Y float64 } //普通的函数 func Distance(p, q Point) float64 { return math.Hypot(q.X - p.X, q.Y - p.Y) } //Point类型的方法,附加的参数p称为方法的接收者,它源自早先的面向对象语言,用来描述主调方法就像向对象发送消息
//Go语言中,接收者不使用特殊名(比如this或者self),而是我们自己选择接收者名字,就像其他的参数变量一样
//调用方法的时候,接收者在方法名的前面,这就和声明保持一致
//方法 func (p Point) Distance(q Point) float64 { return math.Hypot(q.X - p.X, q.Y - p.Y) } func main(){ p := Point{1, 2} q := Point{4, 6}
fmt.Println(Distance(p, q)) //包级别的函数
//调用方法的时候,接收者在方法名的前面,这就和声明保持一致
fmt.Println(p.Distance(q)) //Point类型的方法 //表达式p.Distance称作选择子(selector),它为接收者p选择合适的Distance方法 //选择子也用于选择结构类型中的某些字段值 //编译器会通过方法名和接收者的类型决定调用哪一个函数 //path[i-1]是Point类型,因此调用Point.Distance //perim是Path类型,因此调用Path.Distance perim := Path{ {1, 1}, {5, 1}, {5, 4}, {1, 1}, } fmt.Println(perim.Distance()) } //Path是连接多个点的直线段 type Path []Point //Distance方法返回路径的长度 func (path Path) Distance() float64 { sum := 0.0 for i := range path { if i > 0 { sum += path[i-1].Distance(path[i]) } } return sum } //Path是一个命名的slice类型,而非Point这样的结构体类型,但依旧可以给它定义方法 //Go和许多其他面向对象的语言不同,可以将方法绑定到任何类型上 //可以很方便地为简单的类型(如数字、字符串、slice、map,甚至函数等)定义附加的行为 //同一个包下的任何类型都可以声明方法,只要它的类型既不是指针类型也不是接口类型
使用方法的第一个好处:命名可以比函数更简短。在包的外部进行调用的时候,方法能够使用更加简短的名字且省略包的名字
- 指针接收者的方法
由于主调函数会复制每一个实参变量,如果函数需要更新一个变量,或者一个实参太大我们希望避免复制整个实参,因此必须使用指针来传递变量的地址。这也同样适用于更新接收者:将他绑定到指针类型,比如*Point
func (p *Point) ScaleBy(factor float64) { p.X *= factor p.Y *= factor } //这个方法的名字是(*Point).ScaleBy //圆括号是必须的,没有圆括号,表达式会被解析为*(Point.ScaleBy) //在真实的程序中,习惯上遵循如果Point的任何一个方法使用指针接收者,那么所有的Point方法都应该使用指针接收者,即使有些方法并不一定需要 //命名类型Point与指向它们的指针*Point是唯一可以出现在接收者声明处的类型。为防止混淆,不允许使用本身是指针的类型进行方法声明
/**如果接收者p是Point类型的变量,但方法要求一个*point接收者,可以使用简写:*/ p.ScaleBy(2) /**实际上编译器会对变量进行&p的隐式转换。只有变量才允许这么做,包括结构体字段,像p.X和数组或者slice的元素,比如perim[0]。不能对一个不能取地址的Point接收者参数调用*Point方法,因为无法获取临时变量的地址*/ Point{1,2}.ScaleBy(2)//编译错误:不能获得Point类型字面量的地址 //如果实参接收者是*Point类型,以Point.Distance的方式调用Point类型的方法是合法的,因为我们可以从地址中获取Point的值;只要解引用指向接收者的指针值即可。编译器自动插入一个隐式的*操作符 pptr := &(Point{1, 2}) pptr.Distance(q) //等价于 (*pptr).Distance(q)
//在合法的方法调用表达式中,只有符合下面三种形式的语句才能够成立 func (p Point) Distance(q Point) float64 { return math.Hypot(q.X - p.X, q.Y - p.Y) } func (p *Point) ScaleBy(factor float64) { p.X *= factor p.Y *= factor } //实参接收者和形参接收者是同一个类型 Point{1,2}.Distance(q) //Point pptr.ScaleBy(2) //*Point //实参接收者是T类型,形参接收者是*T类型。编译器会隐式地获取变量的地址 p.ScaleBy(2) //隐式转换为(&p) //实参接收者是*T类型,形参接收者是T类型。编译器会隐式地解析引用接收者,获得实际的值 pptr.Distance(q) //隐式转换为(*pptr)
如果所有类型T方法的接收者是T自己(而非*T),那么复制它的实例是安全的;调用方法的时候都必须进行一次复制。在任何方法的接收者是指针的情况下,应该避免复制T的实例,因为这么做可能会破坏内部原本的数据
nil是一个合法的接收者:就像一些函数允许nil指针作为实参,方法的接收者也一样,尤其是当nil是类型中有意义的零值(如map和slice类型)时,更是如此
//IntList是整型链表 //*IntList的类型nil代表空列表 type IntList struct { Value int Tail *IntList } //Sum返回列表元素的总和 func (list *IntList) Sum() int { if list == nil { return 0 } return list.Value + list.Tail.Sum() } //当定义一个类型允许nil作为接收者时,应当在文档注释中显式地标明
// net/url包种Values类型的部分定义: package url //Values映射字符串到字符串列表 type Values map[string][]string //Get返回第一个具有给定key的值 //如不存在,则返回空字符串 func (v values) Get(key string) string { if vs := v[key];len(vs)>0{
return vs[0]
}
return "" }
//Add添加一个键值到对应key列表中
func (v Values) Add(key, value string) {
v[key] = append(v[key], value)
}
它的实现是map类型但也提供了一系列方法来简化map的操作,它的值是字符串slice,即一个多重map。使用者可以使用它固有的操作方式(make、slice字面量、m[key]),或者使用它的方法,或者同时使用:
m := url.Values{"lang": {"en"}} //直接构造 m.Add("item", "1") m.Add("item", "2") fmt.Println(m.Get("lang")) fmt.Println(m.Get("q")) fmt.Println(m.Get("item")) fmt.Println(m["item"]) m = nil fmt.Println(m.Get("item")) fmt.Println(m["item"]) m = nil fmt.Println(m.Get("item")) m.Add("item", "3") //宕机:赋值给空的map类型
//在最后一个Get调用中,nil接收者充当一个空map。它可以等同地写成Values(nil),Get("item"),但是nil.Get("item")不能通过编译,因为nil的类型没有确定。最后的Add方法会发生宕机因为它尝试更新一个空的map
//因为url.Values是map类型而且map间接地指向它的键/值对,所以url.Values.Add对map中元素的任何更新和删除操作对调用者都是可见的。然而,和普通函数一样,方法对引用本身做的任何改变,比如设置url.Values为nil或者使它指向一个不同的map数据结构,都不会在调用者身上产生作用??
- 通过结构体内嵌组成类型
type Point struct{ X, Y float64 } type ColoredPoint struct { Point Color color.RGBA } //内嵌类型Point,和ColoredPoint并无继承关系
//需要接收Point类型的参数时,如果传入了一个ColoredPoint类型的参数,会报错
//编译错误:不能将ColoredPoint转换为Point类型
内嵌的字段会告诉编译器生成额外的包装方法来调用Point声明的方法,相当于:
func (p ColoredPoint) Distance(q Point) float64 { return p.Point.Distance(q) } func (p *ColoredPoint) ScaleBy(factor float64) { p.Point.ScaleBy(factor) }
匿名字段类型可以是个指向命名类型的指针,这时,字段和方法间接地来自于所指向的对象。这可以让我们共享通用的结构以及使对象之间的关系更加动态、多样化
- 方法变量与表达式
通常都在相同的表达式里使用和调用方法,就像在p.Distance()中,但是把两个操作分开也是可以的。选择子p.Distance可以赋予一个方法变量,它是一个函数,把方法Point.Distance绑定到一个接收者p上。函数只需要提供实参而不需要提供接收者就能够调用
p := Point{1, 2} q := Point{4, 6} distanceFromP := p.Distance //方法变量 选择子 p.Distance fmt.Println(distanceFromP(q)) var origin Point fmt.Println(distanceFromP(origin)) scaleP := p.ScaleBy scaleP(2) //p变成(2,4)
如果包内的API调用一个函数值,并且使用者期望这个函数的行为是调用一个特定接收者的方法,方法变量就非常有用
与方法变量相关的是方法表达式。和调用普通的函数不同,在调用方法的时候必须提供接收者,并且按照选择子的语法进行调用。而方法表达式写成T.f或者(*T).f,其中T是类型,是一种函数变量,把原来方法的接收者替换成函数的第一个形参,因此它可以像平常的函数一样调用
p := Point{1, 2} q := Point{4, 6}
distance := Point.Distance //方法表达式 T.f或者(*T).f,其中T是类型,是一种函数变量,把原来方法的接收者替换成函数的第一个形参
fmt.Println(distance(p, q))
fmt.Printf("%T ", distance)
scale := (*Point).ScaleBy
scale(&p, 2)
fmt.Println(p)
fmt.Printf("%T ", scale)
//如果需要用一个值来代表多个方法中的一个,而方法都属于同一个类型,方法变量可以帮助你调用这个值所对应的方法来处理不同的接收者 type Point struct { X, Y float64} func (p Point) Add(q Point) Point { return Point{ } }
- 示例:位向量
- 封装