方法声明
在函数声明时,在其名字之前放上一个变量,即是一个方法;
package main import "fmt" import "math" type Point struct{ X, Y float64 } // traditional function func Distance(p, q Point) float64 { return math.Hypot(q.X-p.X, q.Y-p.Y) } // same thing, but as a method of the Point type func (p Point) Distance(q Point) float64 { return math.Hypot(q.X-p.X, q.Y-p.Y) } func main() { p := Point{3, 4} q := Point{0, 0} fmt.Println(Distance(p, q)) // "5", function call fmt.Println(p.Distance(q)) // "5", method call }
上面的两个函数调用都是Distance,但是却没有发生冲突。第一个Distance的调用实际上用的是包级别的函数geometry.Distance,而第二个则是使用刚刚声明的Point,调用的是Point类下声明的Point.Distance方法。
定义一个Path类型,这个Path代表一个线段的集合,并且也给这个Path定义一个叫Distance的方法。
// A Path is a journey connecting the points with straight lines. type Path []Point // Distance returns the distance traveled along the path. func (path Path) Distance() float64 { sum := 0.0 for i := range path { if i > 0 { sum += path[i-1].Distance(path[i]) } } return sum }
基于指针对象的方法
当调用一个函数时,会对其每一个参数值进行拷贝,如果一个函数需要更新一个变量,或者函数的其中一个参数实在太大我们希望能够避免进行这种默认的拷贝,这种情况下我们就需要用到指针了。对应到我们这里用来更新接收器的对象的方法,当这个接受者变量本身比较大时,我们就可以用其指针而不是对象来声明方法
func (p *Point) ScaleBy(factor float64) { p.X *= factor p.Y *= factor }
这个方法的名字是(*Point).ScaleBy
。这里的括号是必须的;没有括号的话这个表达式可能会被理解为*(Point.ScaleBy)
在声明方法时,如果一个类型名本身是一个指针的话,是不允许其出现在接收器中的
type P *int func (P) f() { /* ... */ } // compile error: invalid receiver type
想要调用指针类型方法(*Point).ScaleBy
,只要提供一个Point类型的指针即可
package main import "fmt" //import "math" type Point struct{ X, Y float64 } func (p *Point) ScaleBy(factor float64) { p.X *= factor p.Y *= factor } func main() { var p *Point p = &Point{1, 2} p.ScaleBy(2) fmt.Println(*p) // "{2, 4}" r := Point{1, 2} (&r).ScaleBy(2) fmt.Println(r) // "{2, 4}" t := Point{1, 2} t.ScaleBy(2) fmt.Println(t) // "{2, 4}" }
类型 T *T &T都可以调用 对象方法 有点奇怪
实际上每一个合法的方法调用表达式中,也就是下面三种情况里的任意一种情况都是可以的:
接收器的实际参数和其形式参数是相同的类型,比如两者都是类型T或者都是类型*T
:
Point{1, 2}.Distance(q) // Point pptr.ScaleBy(2) // *Point
接收器实参是类型T,但接收器形参是类型*T
,这种情况下编译器会隐式地为我们取变量的地址:
p.ScaleBy(2) // implicit (&p)
接收器实参是类型*T
,形参是类型T。编译器会隐式地为我们解引用,取到指针指向的实际变量:
pptr.Distance(q) // implicit (*pptr)
Nil也是一个合法的接收器类型
一些函数允许nil指针作为参数一样,方法理论上也可以用nil指针作为其接收器,尤其当nil对于对象来说是合法的零值时,比如map或者slice
// An IntList is a linked list of integers. // A nil *IntList represents the empty list. type IntList struct { Value int Tail *IntList } // Sum returns the sum of the list elements. func (list *IntList) Sum() int { if list == nil { return 0 } return list.Value + list.Tail.Sum() }
package url // Values maps a string key to a list of values. type Values map[string][]string // Get returns the first value associated with the given key, // or "" if there are none. func (v Values) Get(key string) string { if vs := v[key]; len(vs) > 0 { return vs[0] } return "" } // Add adds the value to key. // It appends to any existing values associated with key. func (v Values) Add(key, value string) { v[key] = append(v[key], value) }
package main import "fmt" // Values maps a string key to a list of values. type Values map[string][]string // Get returns the first value associated with the given key, // or "" if there are none. func (v Values) Get(key string) string { if vs := v[key]; len(vs) > 0 { return vs[0] } return "" } // Add adds the value to key. // It appends to any existing values associated with key. func (v Values) Add(key, value string) { v[key] = append(v[key], value) } func main() { m :=Values{"lang": {"en"}} // direct construction m.Add("item", "1") m.Add("item", "2") fmt.Println(m.Get("lang")) // "en" fmt.Println(m.Get("q")) // "" fmt.Println(m.Get("item")) // "1" (first value) fmt.Println(m["item"]) // "[1 2]" (direct map access) }
通过嵌入结构体来扩展类型
type Point struct{ X, Y float64 } type ColoredPoint struct { Point Color color.RGBA }
完全可以将ColoredPoint定义为一个有三个字段的struct,但是我们却将Point这个类型嵌入到ColoredPoint来提供X和Y这两个字段,内嵌可以使我们在定义ColoredPoint时得到一种句法上的简写形式,并使其包含Point类型所具有的一切字段,然后再定义一些自己的。如果我们想要的话,我们可以直接认为通过嵌入的字段就是ColoredPoint自身的字段,而完全不需要在调用时指出Point
var cp ColoredPoint cp.X = 1 fmt.Println(cp.Point.X) // "1" cp.Point.Y = 2 fmt.Println(cp.Y) // "2"
red := color.RGBA{255, 0, 0, 255} blue := color.RGBA{0, 0, 255, 255} var p = ColoredPoint{Point{1, 1}, red} var q = ColoredPoint{Point{5, 4}, blue} fmt.Println(p.Distance(q.Point)) // "5" p.ScaleBy(2) q.ScaleBy(2) fmt.Println(p.Distance(q.Point)) // "10"
内嵌可以使我们定义字段特别多的复杂类型,我们可以将字段先按小类型分组,然后定义小类型的方法,之后再把它们组合起来。
一个ColoredPoint并不是一个Point,但他"has a"Point,并且它有从Point类里引入的Distance和ScaleBy方法。从实现的角度来考虑问题,内嵌字段会指导编译器去生成额外的包装方法来委托已经声明好的方法,和下面的形式是等价的:
func (p ColoredPoint) Distance(q Point) float64 { return p.Point.Distance(q) } func (p *ColoredPoint) ScaleBy(factor float64) { p.Point.ScaleBy(factor) }
当Point.Distance被第一个包装方法调用时,它的接收器值是p.Point,而不是p,当然了,在Point类的方法里,你是访问不到ColoredPoint的任何字段的。
类型中内嵌的匿名字段也可能是一个命名类型的指针,这种情况下字段和方法会被间接地引入到当前的类型中(访问需要通过该指针指向的对象去取)。添加这一层间接关系让我们可以共享通用的结构并动态地改变对象之间的关系。
type ColoredPoint struct { *Point Color color.RGBA } p := ColoredPoint{&Point{1, 1}, red} q := ColoredPoint{&Point{5, 4}, blue} fmt.Println(p.Distance(*q.Point)) // "5" q.Point = p.Point // p and q now share the same Point p.ScaleBy(2) fmt.Println(*p.Point, *q.Point) // "{2 2} {2 2}"
一个struct类型也可能会有多个匿名字段。我们将ColoredPoint定义为下面这样:
type ColoredPoint struct { Point color.RGBA }
所以有些 内嵌结构体可以简化:比如
var ( mu sync.Mutex // guards mapping mapping = make(map[string]string) ) func Lookup(key string) string { mu.Lock() v := mapping[key] mu.Unlock() return v } var cache = struct { sync.Mutex mapping map[string]string }{ mapping: make(map[string]string), } func Lookup(key string) string { cache.Lock() v := cache.mapping[key] cache.Unlock() return v }
方法值和方法表达式
常见的p.Distance()形式,实际上将其分成两步来执行也是可能的。p.Distance叫作“选择器”,选择器会返回一个方法“值”->一个将方法(Point.Distance)绑定到特定接收器变量的函数。这个函数可以不通过指定其接收器即可被调用;即调用时不需要指定接收器,只要传入函数的参数即可:
p := Point{1, 2} q := Point{4, 6} distanceFromP := p.Distance // method value fmt.Println(distanceFromP(q)) // "5" var origin Point // {0, 0} fmt.Println(distanceFromP(origin)) // "2.23606797749979", sqrt(5) scaleP := p.ScaleBy // method value scaleP(2) // p becomes (2, 4) scaleP(3) // then (6, 12) scaleP(10) // then (60, 120)
当T是一个类型时,方法表达式可能会写作T.f
或者(*T).f
,会返回一个函数“值”,这种函数会将其第一个参数用作接收器,所以可以用通常(译注:不写选择器)的方式来对其进行调用:
当根据一个变量来决定调用同一个类型的哪个函数时,方法表达式就显得很有用了。你可以根据选择来调用接收器各不相同的方法。下面的例子,变量op代表Point类型的addition或者subtraction方法,Path.TranslateBy方法会为其Path数组中的每一个Point来调用对应的方法:
type Point struct{ X, Y float64 } func (p Point) Add(q Point) Point { return Point{p.X + q.X, p.Y + q.Y} } func (p Point) Sub(q Point) Point { return Point{p.X - q.X, p.Y - q.Y} } type Path []Point func (path Path) TranslateBy(offset Point, add bool) { var op func(p, q Point) Point if add { op = Point.Add } else { op = Point.Sub } for i := range path { // Call either path[i].Add(offset) or path[i].Sub(offset). path[i] = op(path[i], offset) } }