# 方法

方法( method )是与对象实例( instance )相绑定的特殊函数。

方法是面向对象编程的基本概念,用于维护和展示对象自身状态。对象是内敛的,每个实例都有各自不同的独立特征,以属性和方法来对外暴露。普通函数专注于算法流程,接收参数完成逻辑运算,返回结果并清理现场。也就是说,方法有持续性状态,而函数通常没有。

前置接收参数( receiver ),代表方法所属类型。
可为当前包内除接口和指针以外的任何类型定义方法。
不支持静态方法( static method )或关联函数。
不支持重载( overload )。

func (int) test() {} // ~ cannot define new methods on non-local type int
// -------------------------
type N *int
func (N) test() {} // ~ invalid receiver type N (pointer or interface type)
// -------------------------
type M int
func  (M) test(){}
func (*M) test(){} // ~ redeclared in this block

对接收参数命名无限制,按惯例选用简短有意义的名称。
如方法内部不引用实例,可省略接收参数名,仅留类型。

type N int
func (n N) toString() string {
	return fmt.Sprintf("%#x", n)
}
func (N) test() {      // 省略接收参数名。
	println("test")
}
// -----------------------------
func main() {
	var a N = 25
	println(a.toString())
}

接收参数可以是指针类型,调用时据此决定是否复制(pass by value)。

注意区别:
不能为指针和接口定义方法,是说类型 N 本身不能是接口和指针。
这与作为参数列表成员的 receiver *N 意思完全不同。

方法本质上就是特殊函数,接收参数无非是其第一参数。
只不过,在某些语言里它是隐式的 this

type N int
func (n N) copy() {
	fmt.Printf("%p, %v\n", &n, n)
}
func (n *N) ref() {
	fmt.Printf("%p, %v\n", n, *n)
}
func main() {
	var a N = 25
	fmt.Printf("%p\n", &a)  // 0xc000014080
	a.copy()                // 0xc000014088, 25
	N.copy(a)               // 0xc0000140a0, 25
	a++
    
	a.ref()                 // 0xc000014080, 26
	(*N).ref(&a)            // 0xc000014080, 26
}

编译器根据接收参数类型,自动在值和指针间转换。
Go 文档不建议一个结构同时有值接收者和指针接收者

type N int
func (n N) copy() {}
func (n *N) ref() {}
func main() {
	var a N = 25
	var p *N = &a
	a.copy()
    a.ref()     // (*N).ref(&a)
    p.copy()    // N.copy(*p)
	p.ref()
}
/*
$ go build -gcflags "-N -l"
$ go tool objdump -S -s "main\.main" ./test
TEXT main.main(SB)
func main() {
        var a N = 25
  0x455254              MOVQ $0x19, 0x8(SP)     
        var p *N = &a
  0x45525d              LEAQ 0x8(SP), CX        
  0x455262              MOVQ CX, 0x18(SP)       
        a.copy()
  0x455267              MOVQ 0x8(SP), AX        
  0x45526c              CALL main.N.copy(SB)    
        a.ref()
  0x455271              LEAQ 0x8(SP), AX        ; &a
  0x455276              CALL main.(*N).ref(SB)  
        p.copy()
  0x45527b              MOVQ 0x18(SP), CX       
  0x455282              MOVQ 0(CX), AX          
  0x45528a              CALL main.N.copy(SB)    
        p.ref()
  0x45528f              MOVQ 0x18(SP), AX       
  0x455294              CALL main.(*N).ref(SB)  
}
*/

不能以多级指针调用方法。

type N int
func (n N) copy() {}
func (n *N) ref() {}
func main() {
	var a N = 25
	var p *N = &a
	p2 := &p
	// p2.copy() // ~ p2.copy undefined
	// p2.ref() // ~ p2.ref undefined
	(*p2).copy()
	(*p2).ref()
}

如何确定接收参数(receiver)类型?

修改实例状态,用 *T
不修改状态的小对象或固定值,用 T
大对象用 *T ,减少复制成本。
引用类型、字符串、函数等指针包装对象,用 T
含 Mutex 等同步字段,用 *T ,避免因复制造成锁无效。
其他无法确定的,都用 *T

# 匿名嵌入

像访问匿名类型成员那样调用其方法,由编译器负责查找。

type data struct {
	sync.Mutex
	buf [1024]byte
}
func main() {
	d := data{}
	d.Lock() // sync.(*Mutex).Lock()
	defer d.Unlock()
}

同名遮蔽。利用这种特性,实现类似覆盖(override)操作。

和匿名字段访问类似,按最小深度优先原则。
如两个同名方法深度相同,那么编译器无法作出选择(ambiguous selector),需显式指定。

type E struct{}
type T struct {
	E
}
func   (E) toString() string { return "E" }
func (t T) toString() string { return "T." + t.E.toString() }
// ----------------------------------
func main() {
	var t T
	println(t.toString())    // T.E
	println(t.E.toString())  // E
}

同名,但签名不同的方法。

type E struct{}
type T struct {
	E
}
func (E) toString() string { return "E" }
func (T) toString(s string) string { return "T: " + s }
// ------------------------------------------
func main() {
	var t T
	// println(t.toString()) // ~ not enough arguments in call to t.toString
    // 选择深度最小的方法。
	println(t.toString("abc"))   // T: abc
	// 明确目标。
	println(t.E.toString())      // E
}

匿名类型的方法只能访问自己的字段,对外层一无所知。

type E struct {
	x int
}
type T struct {
	x string	
	E
}
func (e *E) do() {
	e.x = 100
	println(e.x)
}
func (t *T) do() {
	t.x = "abc"
	println(t.x)
}
// ------------------------------------------
func main() {
	var t T
	t.do()     // abc
	t.E.do()   // 100
}
/*
可以看出 t.E.do 的 receiver 仅限于自己那段内存。
即便把 E 作第一字断,各自方法内的寻址偏移也指向自己的内存区域。
$ go build -gcflags "-l"
$ go tool objdump -S -s "main\.main" ./test
func main () {
        var t T                                t: T
  0x455314    MOVQ $0x0, 0x8 (SP)        0x8 +---------+
  0x45531d    MOVUPS X15, 0x10 (SP)          | T.x.ptr |
                                       0x10 +---------+
        t.do ()     //abc                   | T.x.len |
  0x455323    LEAQ 0x8 (SP), AX         0x18 +---------+
  0x455328    CALL main.(*T).do (SB)         | T.E.x   |
                                            +---------+
        t.E.do ()   // 100
  0x45532d    LEAQ 0x18 (SP), AX
  0x455332    CALL main.(*E).do (SB)
}
*/

# 方法集

类型有个与之相关的方法集合(method set),这决定了它是否实现某个接口。
根据接收参数(receiver)的不同,可分为 T*T 两种视角。

T.set = T
*T.set = T + *T

type T int
func  (T) A() {}   // 导出成员,否则反射无法获取。
func  (T) B() {}
func (*T) C() {}
func (*T) D() {}
func show(i interface{}) {
	t := reflect.TypeOf(i)
	for i := 0; i < t.NumMethod(); i++ {
		println(t.Method(i).Name)
	}	
}
func main() {
	var n T = 1
	var p *T = &n
	show(n)         //  T = [A, B]
	show(p)         // *T = [A, B, C, D]
}

直接方法调用,不涉及方法集。编译器自动转换所需参数(receiver)。
而转换(赋值)接口(interface)时,须检查方法集是否完全实现接口声明。

type Xer interface {
	B()
	C()
}
type T int
func  (T) A() {}
func  (T) B() {}
func (*T) C() {}
func (*T) D() {}
func main() {
	var n T = 1
    // 方法调用:不涉及方法集。
	n.B()         
	n.C()
    // 接口:检查方法集。
	// var x Xer = n // ~ T does not implement Xer (C method has pointer receiver)
	var x Xer = &n
	x.B()
	x.C()
}

首先,接口会复制对象,且复制品 不能寻址(unaddressable)。
T 实现接口,透过接口调用时, receiver 可被复制,却不能获取指针( &T )。
相反, *T 实现接口,目标对象在接口以外,无论是取值还是复制指针都没问题。
这就是方法集与接口相关,且 T = T*T = T + *T 的原因。

除直属方法外,列表里还包括匿名类型( E )的方法。

T{ E } = T + E
T{ *E } = T + E + *E
*T{ E | *E } = T + *T + E + *E

type E int
func  (E) V() {}
func (*E) P() {}
func show(i interface{}) {
	t := reflect.TypeOf(i)
	for i := 0; i < t.NumMethod(); i++ {
		println("  ", t.Method(i).Name)
	}	
}
func main() {
	println("T{ E }")
	show(struct{E}{})
	println("T{ *E }")
	show(struct{*E}{})
	println("*T{ E }")
	show(&struct{E}{})
	println("*T{ *E }")
	show(&struct{*E}{})
}
//  T{  E }: V
//  T{ *E }: P, V
// *T{  E }: P, V
// *T{ *E }: P, V

别名扩展

通过类型别名,对方法集进行分类,更便于维护。或新增别名,为类型添加扩展方法。

type X int
func (*X) A() { println("X.A") }
type Y = X                           // 别名
func (*Y) B() { println("Y.B") }     // 扩展方法
func main() {
	var x X
	x.A()
	x.B()
	var y Y
	y.A()
	y.B()
}

通过反射,可以看到 “扩展” 被合并的效果。

type X int
func (*X) A() { println("X.A") }
type Y = X
func (*Y) B() { println("Y.B") }
func main() {
	var n X
	t := reflect.TypeOf(&n)
	for i := 0; i < t.NumMethod(); i++ {
		fmt.Println(t.Method(i))
	}
}
// A: func(*main.X)
// B: func(*main.X)

需要注意,不同包的类型可定义别名,但不能定义方法。

type X = bytes.Buffer
// func (*X) B() { println("X.b") }  // ~ cannot define new methods on non-local type

# 方法值

和函数一样,方法除直接调用外,还可赋值给变量,或作为参数传递。
依照引用方式不同,分为表达式(expression)和 值(value) 两种。

type N int
func (n *N) ref() {
	fmt.Printf("%p, %v\n", n, *n)
}
func main() {
	var n N = 100
	// expression
	// var e = (*N).ref
	var e func(*N) = (*N).ref
	e(&n)
	// value
    // var v = n.ref
	var v func() = n.ref
	v()
}
// 0xc000014080, 100
// 0xc000014080, 100

表达式(expr)很好理解,将方法还原为普通函数,显式传递接收参数(receiver)。
而方法值(value)似乎打包了接收参数和方法,导致签名有所不同。

精简代码,看看具体如何实现。

type N int
func (n N) copy() {
	println(n)
}
func (n *N) ref() {
	println(*n)
}
func test(f func()) {
	f()
}
func main() {
	var n N = 100
	var v func() = n.copy
	n++
	n.copy() // 101
	v()      // 100
	test(v)  // 100
}
/*
$ go build -gcflags "-N -l"
$ go tool objdump -S -s "main\.main" ./test
func main() {
    var n N = 100
    0x4552b4     MOVQ $0x64, 0x8(SP)   
    
    var v func() = n.copy
    0x4552c3     LEAQ 0x10(SP), CX               
    0x4552cf     LEAQ N.copy-fm(SB), DX     
    0x4552d6     MOVQ DX, 0x10(SP)               
    0x4552dd     MOVQ 0x8(SP), DX         0x0 +-------------+
    0x4552e2     MOVQ DX, 0x18(SP)            |             |
    0x4552e7     MOVQ CX, 0x20(SP)        0x8 +-------------+
                                              | n = 100     |
    n++                                  0x10 +-------------+     
    0x4552ec     MOVQ 0x8(SP), CX             | copy-fm     |
    0x4552f1     LEAQ 0x1(CX), AX        0x18 +-------------+     
    0x4552f5     MOVQ AX, 0x8(SP)             | 100         |
                                         0x20 +-------------+
    n.copy() // 101                           | ptr -> 0x10 |    
    0x4552fa     CALL N.copy(SB)              +-------------+
    
    v()      // 100
    0x4552ff     MOVQ 0x20(SP), DX
    0x455304     MOVQ 0(DX), CX
    0x455307     CALL CX          ; copy-fm
    
    test(v)  // 100
    0x455309     MOVQ 0x20(SP), AX       
    0x45530e     CALL test(SB)      
}
*/

方法值: funcval { method-fm, receiver-copy }
换成 n.ref ,无非是复制 *N 而已。

TEXT main.N.copy-fm(SB) <autogenerated>
    0x45535d     MOVQ 0x8(DX), AX      // receiver
    0x455361     MOVQ AX, 0x8(SP)
    0x455366     CALL N.copy(SB)
TEXT main.test(SB)
    0x455277     MOVQ 0(AX), CX       // N.copy-fm
    0x45527a     MOVQ AX, DX          // DX
    0x45527d     CALL CX

对于空指针( nil ),注意内存安全。

type N int
func (n N) copy() {}
func (n *N) ref() {}
// -----------------------------
func main() {
	var p *N
	p.ref()         // value
	(*N)(nil).ref() // value
	(*N).ref(nil)   // expression
    
	// p.copy() // ~ runtime error: invalid memory address or nil pointer dereference
    // N.copy(*p) // ~ runtime error: invalid memory address or nil pointer dereference
}
更新于
-->