1. Method Declarations
method 声明是函数声明的变体,额外的参数出现在函数名之前。这个参数将函数关联到该参数的类型上。
gopl.io/ch6/geometry
package geometry
import "math"
type Point struct {X, Y float64}
// traditional function
func Distance(p, q Point) float64 {
return math.Hypot(q.X-p, q.Y-p.Y)
}
// same thing, but as a method of the Point type
func (p Point) Distance(q Point) {
return math.Hypot(q.X-p.X, q.Y-p.Y)
}
额外的参数 p 被称为方法的 receiver,是早期面向对象语言的遗产,将调用方法描述为 “向一个对象发送消息”。
在 Go 中,我们不对 receiver 使用特殊的名字,比如 this 或者 self,我们如同选择其他参数的名字一样选择 receiver 的名字。因为 reveiver 的名字将被频繁使用,选择一个简短的且在不同的方法中具有一致性的名字是一个好主意。常用的选择是类型名字的首字母,比如对 Point 选择 p。
在方法的调用中,receiver 的名字出现在 method 的名字前:
p := Point{1, 2}
q := Point{4, 6}
fmt.Println(Distance(p, q)) // "5", function call
fmt.Println(p.Distance(q)) // "5", method call
第一个声明了 package-level 的函数称为 geometry.Distance。第二个声明类型 Point 的方法,它的名字是 Point.Distance。
表达式 p.Distance 被称为 selector,因为它为类型为 Point 的 receiver p 选择合适的 Distance 方法。Selectors 也被用于选择结构体类型的字段,比如 p.X。因为 methods 和 fields 具有相同的命名空间,所以在结构体 Point 中声明一个方法 X 会有歧义,编译器会拒绝它。
type Path []Point
// Distance returns the distance traveled along the path.
func (path Path) Distance() float64 {
sum := 0.0
for i := range path {
sun += path[i-1].Distance(path[i])
}
return sum
}
Path 是一个命名 slice 类型,不是 struct 类型,我们依然能给它定义 method,Go 允许 method 关联任意类型。Method 能在包内的任意命名类型上声明,只要它的底层类型不是指针或者 interface。
2. Methods with a Pointer Receiver
因为在调用函数时会拷贝每个参数值,如果函数需要更新一个变量,或者参数太大希望能避免拷贝,我们必须使用指针来传递变量的地址。需要更新 receiver 变量的方法同样如此:我们将其关联到指针类型,比如 *Point。
func (p *Point) ScaleBy(factor float64) {
p.X *= factor
p.Y *= factor
}
这一方法的名字是 (*Point).ScaleBy。括号是必要的,没有它们,表达式会被解析为 *(Point.ScaleBy)。
在实际的程序中,公约规定如果任何 Point 的方法有一个指针 receiver,所有的 Point 方法都应当有一个指针的 receiver,即使有道不需要指针 receiver。我们为 Point 打破了这项规则,所以我们展现了两种形式的方法。
命名类型(Point)和指向它们的指针(*Point)是能出现在 receiver 声明中的唯一类型。更进一步,为了避免歧义,方法的声明不允许使用指针类型的命名类型:
type P *int
func (P) f() {/* ... */} // compile error: invalid receiver type
(*Point).ScaleBy 方法能通过提供一个 *Pointer receiver 进行调用,像这样:
r := &Point{1, 2}
r.ScaleBy(2)
fmt.Println(*r) // {2, 4}
或者这样:
p := Point{1, 2}
pptr := &p
pptr.ScaleBy(2)
fmt.Println(p) // "{2, 4}"
或者这样:
p := Point{1, 2}
(&p).ScaleBy(2)
fmt.Printlen(p) // "{2, 4}"
但是后两种情况不够优雅。幸运的是,语言在这一点上会帮助我们。如果 receiver p 是一个变量类型 Point,但是方法需要一个 *Point receiver,我们可以使用这样的简写:
p.ScaleBy
且编译器将对变量执行隐式的 &p。者仅对变量起作用,包括结构体字段比如 p.X 和数组或者 slice 元素比如 perim[0]。我们不能对一个 non-addressable 的 Pointer receiver 调用一个 *Point 方法,因为没有办法获取一个临时值得地址。
Point{1, 2}.ScaleBy(2) //compile error: can't take address of Point literal
但是我们能使用一个 *Point receiver 来调用一个 Point 方法比如 Point.Distance,因为有版本从地址获取值:仅需要加载 receiver 指向得值。编译器隐式得为我们插入一个 * 操作符。这两个函数得调用时等价的:
pptr.Distance(q)
(*pptr).Distance(q)
任意一个 receiver 实参都与 receiver 形参有相同的类型,比如二者都有类型 T 或者都有类型 *T:
Point{1, 2}.Distance(q) // Point
pptr.ScaleBy(2) // *Point
或者 receiver 实参是类型 T 的变量且 receiver 形参的类型为 *T。编译器隐式的取变量地址:
p.ScaleBy(2) // implicit (&p)
或者 receiver 的实参类型为 *T 且 receiver 的形参类型为 T。编译器隐式的对 receiver 进行解引用,换句话说,加载其值:
pptr.Distance(q) //implicit (*pptr)
如果命名类型 T 的所有的方法都有类型为 T 的 receiver,拷贝该类型的实例是安全的;调用它的任意方法必要地进行一次拷贝。例如,time.Duration 值是自由拷贝地,包括作为函数地参数。但是如果有一些方法拥有指针 receiver,你应当避免拷贝 T 的实例,因为这样做可能会破坏内部的不变量。比如,拷贝 bytes.Buffer 的实例将造成原始值和拷贝都作为相同的底层字节数组的别名。随后的方法调用将造成不可预期的影响。
2.1 Nil Is a Valid Receiver Value
正如一些函数允许 nil 指针作为参数,一些方法对它的 receiver 也是如此,特别是当 nil 是有意义的该类型的 zero value,比如 maps 和 slices:
// An IntList is a linked list of integers.
// A nil *IntList represents the empty list.
type InList struct {
Value int
Tail *InList
}
// Sum returns the sum of the list elements.
func (list *IntList) Sum() int {
if list == nil {
returen 0
}
return list.Value + list.Tail.Sum()
}
当你定义一个类型,它的方法允许 nil 作为 receiver 的值时,在文档注释中显式的指出这一点是有价值的:
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)
}
因为 url.Values 是 map 类型且 map 间接的引用它的 key/value 对,任何 url.Value.Add 对 map 中的元素所做的更新和删除都对调用方可见。然而,和普通函数一样,方法对引用本身做出的任何改变,比如将他设置为 nil 或者使他引用不同的 map data structure,将不会反映到调用方(receiver 的类型为 T,而非 *T)。
3. Composing Types by Struct Embedding
假设有一个类型 ColoredPoint:
gopl.io/ch6/coloredpoint
import "image/color"
type Point struct{ X, Y float64 }
type ColorPoint struct {
Point
Color color.RGBA
}
我们定义 ColoredPoint 作为有三个字段的结构体,我们内嵌一个 Point 来提供 X 和 Y 字段。内嵌是我们采取一种语法的快捷方式在 ColoredPoint 中定义 Point 的所有字段:
var cp ColoredPoint
cp.X = 1
fmt.Println(cp.Point.X) // "1"
cp.Point.Y = 2
fmt.Println(cp.Y) // "2"
一个类似的机制应用于 Point 的 methods。我们可以使用 ColoredPoint 类型的 receiver 调用内嵌的 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 的 methods 被 promoted 到 ColoredPoint。以这种方法,内嵌允许具有很多方法的复杂类型能通过几个字段的组合进行构建,每一个提供几个方法。
熟悉基于类的面向对象语言的读者可能尝试将 Point 视为基类,ColoredPoint 视为子类或者派生类,或者解释这些类的关系,好像 ColoredPoint “is a” Point。但这是一个错误。注意上述 Distance 的调用。Distance 有一个类型为 Point 的参数,但 q 不是一个 Point,所以虽然 q 确实有一个该类型的内嵌的字段,我们也必须显式的选择它。尝试传递 q 将会产生一个错误。
p.Distance(q) // compile error: cannot use q(ColoredPoint) as Point
ColoredPoint 不是一个 Point,但他持有一个 Point,且他有两个从 Point 获得的额外的方法 Distance 和 ScaleBy。如果你倾向于考虑实现,内嵌的字段指示编译器生成额外的封装方法,以代理声明的方法,等价于这些:
func (p ColoredPoint) Distance(q Point) float64 {
return p.Point.Distance(q)
}
fun (p *ColoredPoint) ScaleBy(factor float64) {
p.Point.ScaleBy(factor)
}
当 Point.Distance 通过这些封装方法的第一个被调用,它的 receiver 是 p.Point,不是 p,且对于方法来说没有办法去访问 Point 内嵌的 ColoredPoint。
匿名字段的类型可能是命名类型的指针,这种情况下 field 和 method 直接从指向的对象上 promoted。增加另一层的间接性让我们共享常用的结构体,和动态的异化不同对象间的关系。下述 ColoredPoint 的声明嵌入一个 *Point:
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 object
p.ScaleBy(2)
fmt.Println(*p.Point, *q.Point) // {2, 2}, {2, 2}
一个结构体类型可能有超过一个匿名字段。我们可以声明 ColoredPoint 为:
type ColoredPoint struct {
Point
color.RGBA
}
然后这一类型的值将包含所有 Point 的方法,所有 RGBA 的方法,和任何直接声明在 ColoredPoint 上的额外方法。当编译器将 selector 比如 p.ScaleBy 解析为方法时,它首先寻找名为 ScaleBy 的直接声明的方法,然后在 ColoredPoint 中第一层内嵌字段声明的方法中寻找,然后是第二层,即在 Point 和 color.RGBA 中寻找,诸如上述。如果 selector 因为两个方法在一层中被 promoted 而导致有歧义,则编译器报告一个错误。
方法仅能在命名类型(比如 Point)或它们的指针(*Point)上声明,但是感谢内嵌, unnamed 结构体类型也可以有方法,有时这十分有用:
var (
mu sync.Mutex
mapping = make(map[string]string)
)
func Lookup(key string) string {
mu.Lock()
v := mapping[key]
mu.Unlock()
return v
}
下面的版本功能相同,但是将两个相关的变量组合到单个的包层级的变量 cache 中:
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
}
新的变量为关于 cache 的变量赋予了更有表现力的名字,且因为 sync.Mutex 字段是内嵌的,它的 Lock 和 Unlock 方法被 promoted 到了未命名的结构体类型,允许我们使用一个 self-explanatory 的语法来为 cache 加锁。
4. Method Values and Expression
通常我们在相同的表达式中选择和调用一个方法,如同 p.Distance(),但是可以把它们分割成两个动作, selector p.Distance 产生一个method value,将方法绑定到特定 receiver 值 p 的一个函数。这个函数能在没有 receiver 值得情况下调用;它仅需要 non-receiver 参数。
p := Point{1, 2}
q := Point{4, 6}
distanceFromP := p.Distance // method value
fmt.Println(distanceFromP(q)) // "5"
var orgin Point // {0, 0}
fmt.Println(distanceFromP(origin)) // "2.23"
scaleP := p.ScaleBy // method value
scaleP(2) // p becomes (2, 4)
scaleP(3) // then (6, 12)
scaleP(10) // then (60, 120)
当一个 package 的 API 调用一个函数值,且客户端对该函数的的期望行为是调用一个特定 receiver 上的方法时,method value 十分有用。例如,函数 time.AfterFunc 在一个特定的延迟后调用一个函数值。这个程序使用它来在 10 后发射火箭:
type Rocket struct { /* ... */ }
func (r *Rocket) Launch() { /* ... */ }
r := new(Rocket)
time.AfterFunc(10 * time.Second, func() { r.Launch() })
method value 的语法更短:
time.AfterFunc(10 * time.Second, r.Launch)
与 method value 相关的是 method expression。当调用一个方法时,与普通的函数不同,我们必须使用 selector 语法提供一个 receiver。一个 method expression,写作 T.f 或者 (*T).f,T 是一个类型,产生一个函数值,它的常规第一个参数值占据了 receiver 的位置,所以它可以使用通常的方法调用:
p := Point{1, 2}
q := Point{4, 6}
distance := Point.Distance // method expression
fmt.Println(distance(p, q)) // "5"
fmt.Printf("%T\n", distance) // "func(Point, Point) float64"
scale := (*Point).ScaleBy
scale(&p, 2)
fmt.Println(p) // {2 4}
fmt.Println("%T\n", scale) // "func(*Point, float64)"
当你需要一个值来表示属于同一类型的几个方法中的某个选择,这样你可以使用很多不同的 receivers 来调用所选的方法:
type Point struct{X, Y float64}
func (p Point) Add(q Point) { return Point{p.X + q.X, p.Y + q.Y} }
func (p Point) Sub(q 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)
}
}
5. Bit Vector Type
Go 中的 Sets 通常使用 map[T]bool 实现,其中 T 是一个元素类型。由 map 表示的 set 十分灵活,但是,对于特定的问题,一个特化的表示可能更好。例如,在数据流分析领域中,集合元素是小的非负整数,集合有很多元素,集合操作比如联合和插入是十分常见的,一个 bit vector 是理想的。
gopl.io/ch6/intset
// An IntSet is a set of small non-negative integers.
// Its zero value represents the empty set
type IntSet struct {
word []uint64
}
// Has reports whether the set contains the non-negative value x
func (s* IntSet) Has(x int) bool {
word, bit := x/64 uint(x%64)
return word < len(s.words) && s.words[word]&(1 << bit) != 0
}
// Add adds the non-negative value x to the set.
func (s* IntSet) Add(x int) {
word, bit := x/64, uint(x%64)
for word >= len(s.words) {
s.words = append(s.words, 0)
}
s.words[word] = 1 << bit
}
// UnionWith sets s to the union of s and t.
func (s *IntSet) UnionWith(t *IntSet) {
for i, twork := range t.words {
if i < len(s.words) {
s.words[i] |= tword
} else {
s.words = append(s.words, tword)
}
}
}
6. Encapsulation
一个对象的变量或者方法被称为被 encapsulated
,如果它对对象的客户端不可访问。
Go 只有一种机制来控制名字的可见性:大写的标识符从它们被定义的 package 中暴露出来,小写的名字不暴露。结构体的字段和或类型的方法也受与限制 package 成员的访问同样的机制影响。总结,为了封装一个对象,我们必须让它成为结构体。
这种基于名字的机制的封装单元是 package,结构体类型的字段对相同 package 内的所有代码可见,代码出现在函数还是方法中并无差异。