方法声明
附加的参数p,叫做方法的接收器(receiver),在方法调用过程中,接收器参数一般会在方法名之前出现。这和方法声明是一样的,都是接收器参数在方法名字之前。
// 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)
}
调用:
p := Point{1, 2}
q := Point{4, 6}
fmt.Println(Distance(p, q)) // "5", function call
fmt.Println(p.Distance(q)) // "5", method call
第一个Distance的调用实际上用的是包级别的函数geometry.Distance
,而第二个则是使用刚刚声明的Point,调用的是Point类下声明的Point.Distance
方法。
这种p.Distance
的表达式叫做选择器,因为他会选择合适的对应p这个对象的Distance方法来执行。选择器也会被用来选择一个struct类型的字段,比如p.X
。
因为每种类型都有其方法的命名空间,我们在用Distance这个名字的时候,不同的Distance调用指向了不同类型里的Distance方法。
Path是一个命名的slice类型,而不是Point那样的struct类型,然而我们依然可以为它定义方法。
能够给任意类型定义方法
在Go语言里,我们为一些简单的数值、字符串、slice、map来定义一些附加行为很方便。我们可以给同一个包内的任意命名类型定义方法,只要这个命名类型的底层类型(译注:这个例子里,底层类型是指[]Point这个slice,Path就是命名类型)不是指针或者interface:
// A Path is a journey connecting the points with straight lines.
type Path []Point
func (path Path) Distance() float64 {
}
对于一个给定的类型,其内部的方法都必须有唯一的方法名,但是不同的类型却可以有同样的方法名。
基于指针对象的方法
当调用一个函数时,会对其每一个参数值进行拷贝,如果一个函数需要更新一个变量,或者函数的其中一个参数实在太大我们希望能够避免进行这种默认的拷贝,这种情况下我们就需要用到指针了。对应到我们这里用来更新接收器的对象的方法,当这个接受者变量本身比较大时,我们就可以用其指针而不是对象来声明方法。
func (p *Point) ScaleBy(factor float64) {
p.X *= factor
p.Y *= factor
}
这个方法的名字是(*Point).ScaleBy
。这里的括号是必须的;没有括号的话这个表达式可能会被理解为*(Point.ScaleBy)
。
一般会约定如果Point这个类有一个指针作为接收器的方法,那么所有Point的方法都必须有一个指针接收器,即使是那些并不需要这个指针接收器的函数。
只有类型(Point)和指向他们的指针(*Point)
,才可能是出现在接收器声明里的两种接收器。此外,为了避免歧义,在声明方法时,如果一个类型名本身是一个指针的话,是不允许其出现在接收器中的。
type P *int
func (P) f() { /* ... */ } // compile error: invalid receiver type
想要调用指针类型方法(*Point).ScaleBy
,只要提供一个Point类型的指针即可,像下面这样。
r := &Point{1, 2}
r.ScaleBy(2)
fmt.Println(*r) // "{2, 4}"
-
接收器实参是类型T,但接收器形参是类型
*T
,这种情况下编译器会隐式地为我们取变量的地址。编译器会隐式地帮我们用&p去调用
ScaleBy
这个方法。这种简写方法只适用于“变量”,包括struct里的字段比如p.X
,以及array和slice内的元素比如perim[0]
。p := Point{1, 2} p.ScaleBy(2)
我们不能通过一个无法取到地址的接收器来调用指针方法,比如临时变量的内存地址就无法获取得到:
Point{1, 2}.ScaleBy(2) // compile error: can't take address of Point literal
-
接收器实参是类型
*T
,形参是类型T。编译器会隐式地为我们解引用,取到指针指向的实际变量。我们可以用一个
*Point
这样的接收器来调用Point的方法,因为我们可以通过地址来找到这个变量,只要用解引用符号*
来取到该变量即可。编译器在这里也会给我们隐式地插入*
这个操作符,所以下面这两种写法等价的:p := Point{1, 2} pptr := &p pptr.Distance(q) (*pptr).Distance(q)
-
接收器的实际参数和其形式参数是相同的类型,比如两者都是类型T或者都是类型
*T
:Point{1, 2}.Distance(q) // Point pptr.ScaleBy(2) // *Point
总结:
- 不管你的method的receiver是指针类型还是非指针类型,都是可以通过指针/非指针类型进行调用的,编译器会帮你做类型转换。
- 在声明一个method的receiver该是指针还是非指针类型时,你需要考虑两方面的因素,第一方面是这个对象本身是不是特别大,如果声明为非指针变量时,调用会产生一次拷贝;第二方面是如果你用指针类型作为receiver,那么你一定要注意,这种指针类型指向的始终是一块内存地址,就算你对其进行了拷贝。
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()
}
虽然可以通过引用来操作内部值,但在方法想要修改引用本身时是不会影响原始值的,比如把他置换为nil,或者让这个引用指向了其它的对象,调用方都不会受影响。(简单来说就是,传入的是内存地址,在方法里面修改的也只是当前临时变量的指向,置位nil也不会改变原来内存地址存储的值)。
通过嵌入结构体来扩展类型
type Point struct{ X, Y float64 }
type ColoredPoint struct {
Point
Color color.RGBA
}
对于Point中的方法我们也有类似的用法,我们可以把ColoredPoint类型当作接收器来调用Point里的方法,即使ColoredPoint里没有声明这些方法:
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"
Point类的方法也被引入了ColoredPoint
。用这种方式,内嵌可以使我们定义字段特别多的复杂类型,我们可以将字段先按小类型分组,然后定义小类型的方法,之后再把它们组合起来。
一个ColoredPoint
并不是一个Point,但他"has a"Point,并且它有从Point类里引入的Distance
和ScaleBy
方法。
在类型中内嵌的匿名字段也可能是一个命名类型的指针,这种情况下字段和方法会被间接地引入到当前的类型中(译注:访问需要通过该指针指向的对象去取)。添加这一层间接关系让我们可以共享通用的结构并动态地改变对象之间的关系。
type ColoredPoint struct {
*Point
Color color.RGBA
}
当编译器解析一个选择器到方法时,比如p.ScaleBy
,它会首先去找直接定义在这个类型里的ScaleBy
方法,然后找被ColoredPoint
的内嵌字段们引入的方法,然后去找Point和RGBA的内嵌字段引入的方法,然后一直递归向下找。如果选择器有二义性的话编译器会报错,比如你在同一级里有两个同名的方法。
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
叫作“选择器”,选择器会返回一个方法“值”->一个将方法(Point.Distance
)绑定到特定接收器变量的函数。这个函数可以不通过指定其接收器即可被调用;即调用时不需要指定接收器,只要传入函数的参数即可。
p := Point{1, 2}
q := Point{4, 6}
distanceFromP := p.Distance
fmt.Println(distanceFromP(q))
在一个包的API需要一个函数值、且调用方希望操作的是某一个绑定了对象的方法的话,方法“值”会非常实用。
type Rocket struct { /* ... */ }
func (r *Rocket) Launch() { /* ... */ }
r := new(Rocket)
time.AfterFunc(10 * time.Second, func() { r.Launch() })
直接用方法“值”传入AfterFunc的话可以更为简短:
time.AfterFunc(10 * time.Second, r.Launch)
译注:省掉了上面那个例子里的匿名函数。
当T是一个类型时,方法表达式可能会写作T.f
或者(*T).f
,会返回一个函数“值”,这种函数会将其第一个参数用作接收器,所以可以用通常(译注:不写选择器)的方式来对其进行调用。
p := Point{1, 2}
q := Point{4, 6}
distance := Point.Distance
fmt.Println(distance(p, q))
scale := (*Point).ScaleBy
scale(&p, 2)
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}
}
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 {
path[i] = op(path[i], offset)
}
}
#可以改成如下(逻辑不对,这里仅是一种尝试):
func (path Path) TranslateBy(offset Point, add bool) {
var op func(p Point) Point
q := Point{1, 2}
if add {
op = q.Add
} else {
op = q.Sub
}
for i := range path {
path[i] = op(offset)
}
}
示例:Bit数组
Go语言里的集合一般会用map[T]bool
这种形式来表示,T代表元素类型。集合用map类型来表示虽然非常灵活,但我们可以以一种更好的形式来表示它。例如在数据流分析领域,集合元素通常是一个非负整数,集合会包含很多元素,并且集合会经常进行并集、交集操作,这种情况下,bit数组会比map表现更加理想。
一个bit数组通常会用一个无符号数或者称之为“字”的slice来表示,每一个元素的每一位都表示集合里的一个值。当集合的第i位被设置时,我们才说这个集合包含元素i。
因为每一个字都有64个二进制位,所以为了定位x的bit位,我们用了x/64的商作为字的下标,并且用x%64得到的值作为这个字内的bit的所在位置。
**fmt会直接调用用户定义的String方法。**因为我们的String方法定义在IntSet
指针上,所以当我们的变量是IntSet类型而不是IntSet指针时,可能会有下面这样让人意外的情况:
fmt.Println(&x) // "{1 9 42 144}"
fmt.Println(x.String()) // "{1 9 42 144}"
fmt.Println(x) // "{[4398046511618 0 65536]}"
在第一个Println
中,我们打印一个*IntSet
的指针,这个类型的指针确实有自定义的String方法。第二Println
,我们直接调用了x变量的String()方法;这种情况下编译器会隐式地在x前插入&操作符,这样相当于我们还是调用的IntSet指针的String方法。在第三个Println
中,因为IntSet
类型没有String方法,所以Println
方法会直接以原始的方式理解并打印。所以在这种情况下&符号是不能忘的。
-
常用
String()
方法示例:func (s *IntSet) String() string { var buf bytes.Buffer buf.WriteByte('{') for i, word := range s.words { if word == 0 { continue } for j := 0; j < 64; j++ { if word&(1<<uint(j)) != 0 { if buf.Len() > len("{") { buf.WriteByte(' ') } fmt.Fprintf(&buf, "%d", 64*i+j) } } } buf.WriteByte('}') return buf.String() }
封装
一个对象的变量或者方法如果对调用方是不可见的话,一般就被定义为“封装”。
Go语言只有一种控制可见性的手段:大写首字母的标识符会从定义它们的包中被导出,小写字母的则不会。这种限制包内成员的方式同样适用于struct或者一个类型的方法。因而如果我们想要封装一个对象,我们必须将其定义为一个struct。
这种基于名字的手段使得在go语言中最小的封装单元是package
,而不是像其它语言一样的类型。一个struct类型的字段对同一个包的所有代码都有可见性,无论你的代码是写在一个函数还是一个方法里。
只用来访问或修改内部变量的函数被称为setter或者getter,在命名一个getter方法时,我们通常会省略掉前面的Get前缀。