Method in Go
Go 世界里,对象是拥有方法的变量或值,方法是和特定类型绑定的函数。
面向对象的程序使用方法来表示对象(数据结构)的属性和支持的操作,这样就免于直接访问对象的字段(representation)。
方法的 receiver 的叫法来自早期面向对象语言将调用方法描述成“将消息发送给对象”的描述。通常选类型的首字母作为 receiver 的名称。
同名的函数和方法不会冲突,方法有自己的 name space。
一种类型的结构体的字段和方法共用同一个 name space,所以不能重名。
方法可以声明在任何一个定义在同一个包中具名类型中(具名类型的底层类型不能是指针或者接口)。
通常来说,若某个类型的一个方法的 receiver 是指针类型,那么该类型的所有方法都应该使用指针 receiver。
为避免歧义,方法不允许定义在本身(底层类型)是指针类型的具名类型上。
若 p
是类型为 Point
的变量,但方法 ScaleBy
需要的是 *Point
类型的 receiver,可以使用简写 p.ScaleBy()
,编译器会将 p
隐式转换成 &p
。这一语法糖只适用于变量,不能用于不能寻址的(non-addressable)临时值。
Point{1, 2}.ScaleBy() // compile error: can't take address of Point literal
同理编译器也可以隐式添加 *
。
若一个具名类型的所有方法都是 T
类型(不是 *T
),则可以安全地拷贝该类型的实例。若存在一个 receiver 是 pointer 的方法,就应该避免进行实例的拷贝,否则会创建出 alias,容易意外地改变内部数据(violate internal invariants)。
nil 是合法的 receiver 值。若定义了一个允许方法的 receiver 值是 nil 的类型,有必要在文档中说明。
nil 是无类型的值,赋值给有类型的变量后,该变量的类型保持,只是值变成 nil。
匿名结构体字段的语法糖也适用于访问匿名字段类型的方法。匿名字段类型的方法会被提升(promoted),外层结构体类型的 receiver 可以访问到那些方法,借此也可以实现接口的继承。
访问非匿名类型的变量的访问不能使用语法糖,要用完整的写法。
通过这种组合(composition)的方式可以组成拥有很多方法的复杂类型。
type ColoredPoint struct {
Point
color.RGBA
}
// 编译器解析 p.ScaleBy 时,首先直接在 ColoredPoint 类型上找,再往下在 2 个内嵌的匿名字段的类型上找,再往下在匿名字段的类型的字段里找,以此类推。
// 若同一层有两个相同的方法,则编译器报错。https://play.golang.org/p/ZYgiRsuqK4d
方法只能定义在具名字段和指向具名字段的指针上,但使用嵌套可以在不具名类型上添加方法:
// 传统实现
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() // * promoted
return v
}
方法值:
type Rocket struct { /* ... */ }
func (r *Rocket) Launch() { /* ... */ }
r := new(Rocket)
time.AfterFunc(10 * time.Second, func() { r.Launch() })
time.AfterFunc(10 * time.Second, r.Launch) // receiver r 不能改
方法表达式:
p := Point{1, 2}
q := Point{4, 6}
distance := Point.Distance // 方法表达式,Point 是类型不是实例
fmt.Println(distance(p, q)) // receiver(此处是 p)可以任意传
fmt.Printf("%T\n", distance) // "func(Point, Point) float64"
Go 中常用 map[T]bool
实现集合。
bit vector 实现集合(元素是非负整数):
type IntSet struct {
// 第 i 个 bit 是 1 表示集合中存在 i,每 64 bit 算一批
words []uint64
}
// 判断集合中是否存在非负整数 x
func (s *IntSet) Has(x int) bool {
word, bit := x/64, uint(x%64)
// x/64 选出包含判断位的数组元素的下标,len(s.words) 是集合中批次数(决定了能表示的最大数);
// 1<<bit 把第 bit 位设为 1(其它位都是 0),s.words[word] 对应位也为 1 时 != 0 成立,
// 表示集合中存在 x
return word < len(s.words) && s.words[word]&(1<<bit) != 0
}
// 向集合中添加非负整数 x
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
}
// 将集合 t 合并到集合 s
func (s *IntSet) UnionWith(t *IntSet) {
for i, tword := range t.words {
if i < len(s.words) {
s.words[i] |= tword
} else {
s.words = append(s.words, tword)
}
}
}
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()
}
var x, y IntSet
x.Add(1)
x.Add(144)
x.Add(9)
fmt.Println(x.String()) // "{1 9 144}"
fmt.Println(&x) // {1 9 144}
fmt.Println(x) // {[514 0 65536]}
y.Add(9)
y.Add(42)
fmt.Println(y.String()) // "{9 42}"
x.UnionWith(&y)
fmt.Println(x.String()) // "{1 9 42 144}"
fmt.Println(x.Has(9), x.Has(123)) // "true false"
// https://play.golang.org/p/Wc-Rf3RXFz-
封装(encapsulation):外界不能访问对象的字段或方法。
Go 中只有标识符的首字母大写的包成员、结构体字段、方法,外界才能访问。
// 同一个包中的代码可以修改 words 字段
type IntSet struct {
words []uint64
}
// 包以外的代码可以随意修改 IntSet 类型的变量
type IntSet []uint64
封装的优点:
- 值的变化更容易溯源;
- 隐藏实现细节,防止 clients 依赖具体实现,开发者可以更好地迭代;
- 防止外部代码的任意修改。
封装不是通解,time.Duration
将内部 int64
(纳秒)的表示暴露出来,使得 time.Duration
支持常用的四则运算和比较运算,也可以定义此类型的常量。
const day = 24 * time.Hour
fmt.Println(day.Seconds()) // "86400"