go——方法
方法是与对象实例绑定的特殊函数。
方法是面向对象编程的基本概念,用于维护和展示对象的自身状态。
对象是内敛的,每个实例都有各自不同的独立特征,以属性和方法来暴露对外通信接口。
普通函数则专注于算法流程,通过接收参数来完成特定逻辑算法,并最终返回结果。
换句话说,方法是有关联状态的,而函数通常没有。
方法和函数定义语法区别在于前者有前置实例接收参数(receiver),编译器以此确定方法所属类型。
在某些语言里,尽管没有显式定义,但会在调用时隐式传递this实例参数。
可以为当前包以及除接口和指针之外的任何类型定义方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | package main import "fmt" type N int //自定义类型 func (n N) toString() string { //方法本质上就是绑定在某个实例上的函数,与函数相比就是多了一个接收参数,来说明方法所属。 return fmt.Sprintf( "%#x" , n) } func main() { var a N = 25 //调用方法必须有实例对象 fmt.Println(a.toString()) //0x19 } |
方法同样不支持重载(overload)。receiver参数名没有限制,按惯例会选用简短有意义的名称。
如果方法内部并不引用实例,可省略参数名,仅保留类型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | package main import "fmt" type N int func (N) test() { fmt.Println( "hello, world" ) } func main() { var n N = 3 n.test() } |
方法可以看作特殊函数,那么receiver的类型自然可以看作是基础类型或指针类型。
这会关系到调用时对象实例是否被复制。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | package main import "fmt" type N int func (n N) value() { n++ fmt.Printf( "v: %p, %v\n" , &n, n) } func (n *N) pointer() { //指针类型 (*n)++ //通过指针反取得到数据 fmt.Printf( "p: %p, %v\n" , n, *n) } func main() { var a N = 10 a.value() //值传递,复制 a.pointer() //指针引用传递,共用 fmt.Printf( "a: %p, %v\n" , &a, a) } /* v: 0xc00004e088, 11 p: 0xc00004e080, 11 //通过指针传值,其实是指向原数据 a: 0xc00004e080, 11 */ |
(n N)与(n *N):定义的是n N,那么只能说明指针是一个值的基础数据。
可使用实例值或指针调用方法,编译器会根据方法的receiver类型自动在基础类型和指针类型间转换。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | package main import "fmt" type N int func (n N) value() { n++ fmt.Printf( "v: %p, %v\n" , &n, n) } func (n *N) pointer() { (*n)++ fmt.Printf( "p: %p, %v\n" , n, *n) } func main() { var a N = 10 p := &a fmt.Printf( "a: %p, %v\n" , &a, a) a.value() a.pointer() //这里虽然传递的是整数类型,但是编译器会自动判断 p.value() p.pointer() fmt.Printf( "a: %p, %v\n" , &a, a) } /* a: 0xc00004e080, 10 v: 0xc00004e098, 11 //复制值,+1,并没有改变原数据,变量指向了另一个内存地址 p: 0xc00004e080, 11 //指针传递,+1,改变了元数据 v: 0xc00004e0d0, 12 //11 + 1 p: 0xc00004e080, 12 //11 + 1 a: 0xc00004e080, 12 //使用指针传递的时候,值改变并不会改变变量的内存地址 */ |
需要注意的是不能用多层指针调用方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | package main import "fmt" type N int func (n N) value() { n++ fmt.Printf( "v: %p, %v\n" , &n, n) } func (n *N) pointer() { (*n)++ fmt.Printf( "p: %p, %v\n" , n, *n) } func main() { var a N = 25 p := &a p2 := &p p2.value() //calling method value with receiver p2 (type **N) requires explicit dereference p2.pointer() } |
指针类型的receiver必须是合法指针(包括nil),或能获取实例地址。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | package main import "fmt" type X struct {} func (x, *X) test() { fmt.Println( "hi!" ) } func main() { var a *X X{}.test() //结果体没有指针属性 } |
可以像访问匿名字段成员那样调用方法,由编译器负责查找。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | package main import ( "sync" ) type data struct { sync.Mutex buf [1024]byte } func main() { d := data{} d.Lock() //直接调用结构体字段的方法 defer d.Unlock() } |
方法也会有同名遮蔽问题。但利用这种特性,可实现类似覆盖操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | package main import "fmt" type user struct {} type manager struct { user } func (user) toString() string { return "user" } func (m manager) toString() string { return m.user.toString() + ",manager" } func main() { var m manager fmt.Println(m.toString()) //user,manager fmt.Println(m.user.toString()) //user } |
尽管可以直接访问匿名字段的成员和方法,但是它们依然不属于继承关系。
类型有一个与之相关的方法集,这决定了它是否实现了某个接口
类型T方法集包含所有receiver T方法
类型*T方法集包含所有receiver T + *T方法
匿名嵌入S,T方法集包含所有receiver S方法
匿名嵌入*S, T方法集包含所有receiver S + *S方法
匿名嵌入S或*S, *T方法集包含所有receiver S + *S
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | package main import ( "fmt" "reflect" ) type S struct {} type T struct { S } func (S) sVal() {} func (*S) sPtr() {} func (T) tVal() {} func (*S) tPtr() {} func methodSet(a interface {}) { t := reflect.TypeOf(a) for i, n := 0, t.NumMethod(); i < n; i++ { m := t.Method(i) fmt.Println(m.Name, m.Type) } } func main() { var t T methodSet(t) fmt.Println( "-------------" ) methodSet(&t) } |
方法集仅影响接口实现和方法表达式转换,与通过实例或实例指针调用方法无关。
实例并不使用方法集,而是直接调用(或通过隐式字段名。
面向对象的三大特征“封装”、“继承”、“多态”,go语言仅实现了部分特征,它更倾向于“组合大于继承”这种思想。
将模块分解成相互独立的更小单元,分别处理不同方面的需求,最后以匿名嵌入组合到一起。
而其简短一致的调用方式,更是隐藏了内部实现细节
没有父子组合依赖,不会破坏封装。且整体和局部松耦合,可任意增加来实现扩展。
各单元持有单一责任,互无关联,实现和维护更加简单。
方法和函数一样,除直接调用外,还可以赋值给变量,或作为参数传递。
依照具体引用的方式不同,可分为expression和value两种状态。
通用类型引用的method expression会被还原成普通函数样式,receiver是第一参数,调用时须显式传参。
至于类型,可以是T或*T,只要目标方法存在于该类型方法集中即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | package main import "fmt" type N int func (n N) test() { fmt.Printf( "test.n: %p, %d\n" , &n, n) } func main() { var n N = 25 fmt.Printf( "main.n: %p, %d\n" , &n, n) f1 := N.test //直接将方法赋值给变量 f1(n) f2 := (*N).test f2(&n) N.test(n) //直接以表达式方式调用 (*N).test(&n) } /* main.n: 0xc00004e080, 25 test.n: 0xc00004e098, 25 test.n: 0xc00004e0b8, 25 */ |
基于实例或指针引用的method value,参数签名不不会改变,依旧按正常方式调用。
但当method value被赋值给变量或作为参数传递时,会立即计算并赋值该方法执行所需的receiver对象
与其绑定,以便在稍后执行,能隐式传入receiver参数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | package main import "fmt" type N int func (n N) test() { fmt.Printf( "test.n: %p, %v\n" , &n, n) } func main() { var n N = 100 p := &n n++ f1 := n.test n++ f2 := p.test n++ fmt.Printf( "main.n: %p, %v\n" , p, n) f1() f2() } /* main.n: 0xc00000a168, 103 test.n: 0xc00000a1a0, 101 test.n: 0xc00000a1b0, 102 */ |
编译器会为method value生成一个包装函数,实现间接调用。
至于receiver复制,和闭包的实现方法基本一致,打包成funcval,经由DX寄存器传递。
当method value作为参数时,会复制给receiver在内的整个method value。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | package main import "fmt" type N int func call(m func ()) { m() } func (n N) test() { fmt.Printf( "test.n: %p, %v\n" , &n, n) } func main() { var n N = 100 p := &n fmt.Printf( "main.n: %p, %v\n" , p, n) n++ call(n.test) n++ call(p.test) } /* main.n: 0xc00004e080, 100 test.n: 0xc00004e098, 101 test.n: 0xc00004e0b8, 102 */ |
如果目标方法的receiver是指针类型,那么被复制的仅是指针。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | package main import "fmt" type N int func (n *N) test() { fmt.Printf( "test.n: %p, %v\n" , n, *n) } func main() { var n N = 100 p := &n n++ f1 := n.test n++ f2 := p.test n++ fmt.Printf( "main.n: %p, %v\n" , p, n) f1() f2() } /* main.n: 0xc00004e080, 103 test.n: 0xc00004e080, 103 test.n: 0xc00004e080, 103 */ |
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· 终于写完轮子一部分:tcp代理 了,记录一下
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理