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

封装的优点:

  1. 值的变化更容易溯源;
  2. 隐藏实现细节,防止 clients 依赖具体实现,开发者可以更好地迭代;
  3. 防止外部代码的任意修改。

封装不是通解,time.Duration 将内部 int64(纳秒)的表示暴露出来,使得 time.Duration 支持常用的四则运算和比较运算,也可以定义此类型的常量。

const day = 24 * time.Hour
fmt.Println(day.Seconds()) // "86400"