方法声明
方法的声明和普通函数的声明类似,只是在函数名字前面多了一个参数。这个参数把这个方法绑定到这个参数对应的类型上。
指针接收者的方法
命名类型(Point) 与指向它们的指针(*Point)是唯一可以出现在接收者声明处的类型。而且,为防止混淆,不允许本身是指针的类型进行方法声明:
type p *int
func (p) f() {
} // 编译错误:非法的接收者类型
实际上编译器会对变量进行 &p 的隐式转换。只有变量才允许这么做,包括结构体字段,像 p.X 和数组或者 slice 的元素,比如 perim[0] 。不能够对一个不能取地址的 Point 接收者参数调用 *Point 方法,因为无法获取临时变量的地址。
实参接收者和形参接收者是同一个类型,比如都是 T 类型或都是 *T 类型。
或者实参接收者是 T 类型的变量而形参接收者是 *T 类型。编译器会隐式地获取变量的地址。
或者实参接收者是 *T 类型而形参接收者是 T 类型。编译器会隐式地获取实际的取值。
位向量
在数据流分析领域,集合元素都是小的非负整型,集合拥有许多元素,而且集合的操作多数是求并集和交集,位向量是个理想的数据结构。
位向量使用一个无符号整型值的 slice,每一位代表集合中的一个元素。如果设置第 i 位的元素,则认为集合包含 i 。
package main
import (
"bytes"
"fmt"
)
// IntSet 是一个包含非负整数的集合
// 零值代表空的集合
type IntSet struct {
words []uint64
}
// Has 方法的返回值表示是否存在非负整数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 添加非负整数 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
}
// UnionWith 将会对s和t 做并集并将结果存在 s 中
func (s *IntSet) UnionWith(t *IntSet) {
for i, word := range t.words {
if i < len(s.words) {
s.words[i] |= word
} else {
s.words = append(s.words, word)
}
}
}
// String 方法以字符串 "{1 2 3}" 的形式返回集中
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()
}
在IntSet 集合中,由于每一个字拥有 64 位,因此为了定位 x 的位置,我们使用商数 x/64 作为字的索引,而 x % 64 记做该字内的索引。UnionWith 操作使用按位 “或” 操作符 | 来计算一次64个元素求并集的结果。
var x, y IntSet
x.Add(1)
x.Add(144)
x.Add(9)
fmt.Println(x.String()) // "{1 9 144}"
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"
fmt.Println(&x) // "{1 9 42 144}"
fmt.Println(x.String()) // "{1 9 42 144}"
fmt.Println(x) // "{[4398046511618 0 65536]}"
fmt 在输出 &x 的时候,输出了 *IntSet 指针,它有一个 String 方法。fmt 在输出 x.String 的时候,编译器会帮我们隐式地插入 &操作符,我们得到指针后就可以获取到 String 方法里了。在 fmt 输出 x 的时候,因为 IntSet 值本身没有 String 方法,所以 fmt 直接输出了结构体。
封装
Go 语言只有一种方法控制命名的可见性:定义的时候,首字母大写的标识符是可以从包中导出的,而首字母没有大写的则不导出。同样的机制也同样作用于结构体内的字段和类型中的方法。结论就是,要封装一个对象,必须使用结构体。