Methods in Go

本文介绍了Go语言中方法声明的变体,包括receiver的概念,以及如何通过方法实现面向对象编程。文中详细阐述了方法的调用方式,指针receiver的作用,以及内嵌类型在方法调用中的应用。此外,还讨论了封装,展示了如何通过结构体字段和方法的封装来创建复杂类型。
摘要由CSDN通过智能技术生成

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 内的所有代码可见,代码出现在函数还是方法中并无差异。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值