百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术分类 > 正文

golang 方法和方法集

ztj100 2025-01-10 18:39 15 浏览 0 评论

方法

概述: 在面向对象编程, 一个对象其实也就是一个简单的值或者一个变量, 在这个对象中会包含一些函数, 这种带有接收者的函数, 我们称为为方法(method)。

本质上, 一个方法则是一个和特殊类型关联的函数。

一个面向对象的程序会用方法来表达其属性和对应的操作, 这样使用这个对象的用户就不需要直接去操作对象, 而是借助方法来做这些事情。

在Go语言中, 可以给任意自定义类型(包括内置类型, 但不包括指针类型)添加相应的方法。

方法总是绑定对象实例, 并隐式将实例作为第一实参(receiver), 方法的语法如下:

func (receiver ReceiverType) funcName(paramters) (results)

参数receiver可任意命名。 如方法中未曾使用, 可省略参数名。 receiver: 接收者 [r??si?v?(r)]

参数receiver类型可以是 T 或 *T。 基类型 T 不能是接口或指针

不支持重载方法, 也就是说, 不能定义名字相同但是不同参数的方法。

注意:

每个方法只能有一个 receiver 参数, 不支持多 receiver 参数列表或变长 receiver 参数。

一个方法只能绑定一个基类型, Go 语言不支持同时绑定多个类型的方法。

receiver 参数的基类型本身不能是指针类型或接口类型。

如: 下面的 receiver参数是无效的

type MyInt *int
func (r MyInt) String() string { // invalid receiver type MyInt (MyInt is a pointer type)
    return fmt.Sprintf("%d", *(*int)(r))
}
type MyReader io.Reader
func (r MyReader) Read(p []byte) (int, error) { // invalid receiver type MyReader (MyReader is an interface type)
    return r.Read(p)
}

Go 方法具有如下特点:

方法名的首字母是否大写决定了该方法是否是导出方法 ;

方法定义要与类型定义放在同一个包内。

由于方法定义与类型定义必须放在同一个包下面, 因此我们可以推论得到:我们不能为原生类型(诸如:int、float64、map 等)添加方法, 只能为自定义类型定义方法。

错误的作法:

func (i int) String() string { // cannot define new methods on non-local type int
    return fmt.Sprintf("%d", i)
}
正确的作法:
type MyInt int
func (i MyInt) String() string {
    return fmt.Sprintf("%d", int(i))
}

常见的问题: cannot define new methods on non-local type int

原因: go语言不允许为简单的内置类型添加方法, 一般用于自定义结构体 struct, 其他数据类型不推荐使用;

为类型添加方法

基础类型作为接收者

package main //必须有个main包
import "fmt"
type MyInt int //自定义类型, 给int改名为MyInt, 定义方法必须使用自定义类型或者结构体
//在函数定义时, 在其名字之前放上一个变量, 即是一个方法
func (a MyInt) Add(b MyInt) MyInt { //面向对象
    return a + b
}
//传统方法定义
func Add(a, b MyInt) MyInt {
    return a + b
}
func main(){
    var a MyInt = 1
    var b MyInt = 1

    //调用func (a MyInt) Add(b MyInt)
    fmt.Println("a.Add(b) = ", a.Add(b)) //a.Add(b) == 2

    //调用func Add(a, b MyInt)
    fmt.Println("Add(a, b) = ", Add(a, b)) //Add(a, b) = 2
}

通过上面的例子可以看出, 面向对象只是换了一种语法形式来表达。方法是函数的语法糖, 因为receiver其实就是方法所接收的第1个参数。

注意: 虽然方法的名字一模一样, 但是如果接收者不一样, 那么方法就不一样。

package main //必须有个main包
import "fmt"
type MyInt int //自定义类型, 给int改名为MyInt, 定义方法必须使用自定义类型或者结构体
//在函数定义时, 在其名字之前放上一个变量, 即是一个方法
func (a MyInt) Add(b MyInt, c MyInt) MyInt { //面向对象 参数可以多出一个, 不会报bug
    return a + b
}
//传统方法定义
func Add(a, b, c MyInt) MyInt { // 参数可以多出一个, 不会报bug
    return a + b
}
func main() {
    var a MyInt = 1
    var b MyInt = 1
    //调用func (a MyInt) Add(b MyInt)
    fmt.Println("a.Add(b) = ", a.Add(b, 10)) //a.Add(b) == 2
    //调用func Add(a, b MyInt)
    fmt.Println("Add(a, b) = ", Add(a, b, 10)) //Add(a, b) = 2
}

注意: 函数和方法多出的参数没有使用, 不会报bug(如: 参数c);

结构体作为接收者

方法里面可以访问接收者的字段, 调用方法通过点(.)访问, 就像struct里面访问字段一样:

package main //必须有个main包
import "fmt"
type Person struct {
    name string
    sex byte
    age int
}
func (p Person) PrintInfo(){
    fmt.Println(p.name, p.sex, p.age)
}
func main(){
    p := Person{"mike", 'm', 18} //初始化
    p.PrintInfo() //调用func (p Person) PrintInfo()
}

输出结果:

mike 109 18

值语义和引用语义

package main //必须有个main包
import "fmt"
type Person struct {
    name string
    sex byte
    age int
}
//指针作为接收者, 引用语义
func (p *Person) SetInfoPointer(){
    //给成员赋值
    (*p).name = "yoyo"
    p.sex = 'f'
    p.age = 22
}
//值作为接收者, 值语义
func (p Person) SetInfoValue(){
    //给成员赋值
    p.name = "yoyo"
    p.sex = 'f'
    p.age = 22
}
func main(){
    //指针作为接收者, 引用语义
    p1 := Person{"mike", 'm', 18} //初始化
    fmt.Println("函数调用前 = ", p1) //函数调用前 = {mike 109 18}

    p1.SetInfoPointer() // 这里没有使用指针不会改变接收者(receiver)是引用语义, p 会转换成 &p
    fmt.Println("函数调用后 = ", p1) //函数调用后 = {yoyo 102 22}
    (&p1).SetInfoPointer()
    fmt.Println("函数调用后 = ", p1) //函数调用后 = {yoyo 102 22}

    fmt.Println("==========")

    //值类型作为接收者, 值语义
    p2 := Person{"mike", 'm', 18} //初始化
    fmt.Println("函数调用前 = ", p2) //函数调用前 = {mike 109 18}

    p2.SetInfoValue()
    fmt.Println("函数调用后 = ", p2) //函数调用后 = {mike 109 18} 值语义不会改变原有的值

    (&p2).SetInfoValue() // 这里的 "&" 不会改变接收者(receiver)是值语义, &p 会转换成 p
    fmt.Println("函数调用后 = ", p2) //函数调用后 = {mike 109 18} 值语义不会改变原有的值
}

由此可见: 决定值语义还是引用语义, 在于定义方法是否作为指针作为接收者;

接收者(receiver)究竟是指针(引用语义)还是值(值语义)类型, 是在定义时候决定, 而不是在使用的时候决定;

值语义和引用语义的区别:

Go 语言中的大多数类型都是值语义。值语义和引用的区别在于赋值之后, 重新赋值, 是否会改变原值。

如果不改变原值, 则是值语义。否则是引用语义, 引用语义比值语义拥有更复杂的存储结构。比如分配内存、指针、长度、容量等。

总结:

值接收者(值语义) vs 指针接收者(引用语义)

要改变内容必须使用指针接收者

结构过大也考虑使用指针接收者

一致性: 如有指针接收者, 最好都是指针接收者

值接收者是go语言特有

值/指针接收者均可接收值/指针

方法集

类型的方法集是指可以被该类型的值调用的所有方法的集合。

用实例 value 和 pointer 调用方法(含匿名字段)不受方法集约束, 编译器总是查找全部方法, 并自动转换 receiver 实参。

类型 *T 方法集

一个指向自定义类型的值的指针, 它的方法集由该类型定义的所有方法组成, 无论这些方法接受的是一个值还是一个指针。

如果在指针上调用一个接受值的方法, Go语言会聪明地将该指针解引用, 并将指针所指的底层值作为方法的接收者。

类型 *T 方法法集包含全部 receiver T + *T 方法:

package main //必须有个main包
import "fmt"
type Person struct {
    name string
    sex byte
    age int
}
//指针作为接收者, 引用语义
func (p *Person) SetInfoPointer() {
    (*p).name = "yoyo"
    p.sex = 'f'
    p.age = 22
    fmt.Println("SetInfoPointer")
}
//值作为接收者, 值语义
func (p Person) SetInfoValue(){
    p.name = "xxx"
    p.sex = 'm'
    p.age = 33
    fmt.Println("SetInfoValue")
}
func main(){
    //p为指针类型
    var p *Person = &Person{"mike", 'm', 18} // 此时的"*"代表指针类型
    p.SetInfoPointer() //SetInfoPointer 内部将p转化为*p, 再调用
    (*p).SetInfoPointer() //SetInfoPointer 等同于 p.SetInfoPointer(), 但是效率高, 此时的"*"代表操作符, 因为 p 传递的是指针数据类型

    p.SetInfoValue() //SetInfoValue 同于 (*p).SetInfoValue(), 但是效率高
    (*p).SetInfoValue() //SetInfoValue 内部将*p转化为p, 再调用
}

类型 T 方法集

一个自定义类型值的方法集则由为该类型定义的接收者类型为值类型的方法组成, 但是不包含那些接收者类型为指针的方法。

但这种限制通常并不像这里所说的那样, 因为如果我们只有一个值, 仍然可以调用一个接收者为指针类型的方法, 这可以借助于Go语言传值的地址能力实现。

package main //必须有个main包
import "fmt"
type Person struct {
    name string
    sex byte
    age int
}
//指针作为接收者, 引用语义
func (p *Person) SetInfoPointer() {
    (*p).name = "yoyo" // * 代表操作符
    p.sex = 'f'
    p.age = 22
    fmt.Println("SetInfoPointer")
}
//值作为接收者, 值语义
func (p Person) SetInfoValue(){
    p.name = "xxx"
    p.sex = 'm'
    p.age = 33
    fmt.Println("SetInfoValue")
}
func main(){
    //p为普通类型
    p := Person{"mike", 'm', 18}
    (&p).SetInfoPointer() //SetInfoPointer //代表传址
    (p).SetInfoPointer() //SetInfoPointer 内部先把p转化为&p后, 再调用

    p.SetInfoValue() //SetInfoValue
    (&p).SetInfoValue() //SetInfoValue 内部先把&p转化为p后, 再调用
    //(*p).SetInfoPointer() //err, invalid indirect of s (type Person)
    //(*s).SetInfoValue() //err, invalid indirect of s (type Person)
    fmt.Println(p.name, p.age, p.sex) //yoyo 22 102
}

匿名字段

方法的继承

如果匿名字段实现了一个方法, 那么包含这个匿名字段的struct也能调用这个方法

package main //必须有个main包
import "fmt"
type Person struct {
    name string
    sex byte
    age int
}
//Person定义了方法
func (p *Person) PrintInfo() {
    fmt.Printf("%s, %c, %d\n", p.name, p.sex, p.age)
}
type Student struct {
    Person //匿名字段, 那么 Student 包含了 Person 的所有字段
    id int
    addr string
}
func main(){
    p := Person{"mike", 'm', 18}
    p.PrintInfo() //mike, m, 18

    s := Student{Person{"yoyo", 'f', 20}, 2, "sz"}
    s.PrintInfo() //yoyo, f, 20
}

方法的重写

package main //必须有个main包
import "fmt"
type Person struct {
    name string
    sex byte
    age int
}
//Person定义了方法
func (p *Person) PrintInfo() {
    fmt.Printf("Person: %s, %c, %d\n", p.name, p.sex, p.age)
}
type Student struct {
    Person //匿名字段, 那么 Student 包含了 Person 的所有字段\
    id int
    addr string
}
func (s *Student) PrintInfo() {
    fmt.Printf("Student: %s, %c, %d\n", s.name, s.sex, s.age)
}
func main(){
    p := Person{"mike", 'm', 18}
    p.PrintInfo() //Person: mike, m, 18

    s := Student{Person{"yoyo", 'f', 20}, 2, "sz"}
    s.PrintInfo() //Student: yoyo, f, 20
    s.Person.PrintInfo() //Person: yoyo, f, 20
}

表达式

类似于我们可以对函数进行赋值和传递一样, 方法也可以进行赋值和传递。

根据调用者不同, 方法分为两种表现形式, 方法值和方法表达式。 两者都可像普通函数那样赋值和传参, 区别在于方法值绑定实例, 而方法表达式则须显式传参。

方法值

package main //必须有个main包
import "fmt"
type Person struct {
    name string
    sex byte
    age int
}
//Person定义了方法
func (p *Person) PrintInfoPointer() {
    fmt.Printf("%p, %v\n", p, p)
}
func (p Person) PrintInfoValue() {
    fmt.Printf("%p, %v\n", &p, p)
}
func main(){
    p := Person{"mike", 'm', 18}
    p.PrintInfoPointer() //0xc0000640c0, &{mike 109 18}

    pFunc1 := p.PrintInfoPointer //方法值, 隐式传递receiver
    pFunc1() //0xc0000640c0, &{mike 109 18}

    pFunc2 := p.PrintInfoValue
    pFunc2() //0xc000064140, {mike 109 18}
}

方法表达式

package main //必须有个main包
import "fmt"
type Person struct {
    name string
    sex byte
    age int
}
//Person定义了方法
func (p *Person) PrintInfoPointer() {
    fmt.Printf("%p, %v\n", p, p)
}
func (p Person) PrintInfoValue() {
    fmt.Printf("%p, %v\n", &p, p)
}
func main(){
    p := Person{"mike", 'm', 18}
    p.PrintInfoPointer() //0xc0000640c0, &{mike 109 18}

    //方法表达式, 须显式传参
    pFunc1 := (*Person).PrintInfoPointer
    pFunc1(&p) //0xc000004460, &{mike 109 18}

    pFunc2 := Person.PrintInfoValue
    pFunc2(p) //0xc0000044e0, {mike 109 18}
}

总结:

结构体的方法集和非结构体的方法集区别

结构体的方法集

package main
import (
    "fmt"
)
type T struct {
    int
}
func (t T) test() {
    fmt.Println("类型 T 方法集包含全部 receiver T 方法。")
}
func main() {
    t1 := T{1}
    fmt.Printf("t1 is : %v\n", t1)
    t1.test()
}

非结构体的方法集

package main
import (
"fmt"
)
type T int
func (t T) test() {
    fmt.Println("类型 T 方法集包含全部 receiver T 方法。")
}
func main() {
    var t1 T
    t1 = 10
    fmt.Printf("t1 is : %v\n", t1)
    t1.test()
}

隐式传递和显示传递区别

package main
import "fmt"
type User struct {
    id int
    name string
}
func (self *User) Test() {
    fmt.Printf("%p, %v\n", self, self)
}
func main() {
    u := User{1, "Tom"}
    u.Test()
    mValue := u.Test
    mValue() // 隐式传递 receiver
    mExpression := (*User).Test
    mExpression(&u) // 显式传递 receiver
}

立即复制 receiver, 因为不是指针类型, 不受后续修改影响。

package main
import "fmt"
type User struct {
    id int
    name string
}
func (self User) Test() {
    fmt.Println(self)
}
func main() {
    u := User{1, "Tom"}
    mValue := u.Test // 立即复制 receiver,因为不是指针类型,不受后续修改影响。
    u.id, u.name = 2, "Jack"
    u.Test()
    mValue()
}

Go 方法的本质:一个以方法所绑定类型实例为第一个参数的普通函数;

Go 语法甜头使得我们通过类型实例调用类型方法时无需考虑实例类型与 receiver 参数类型是否一致, 编译器会为我们做自动转换;

receiver 参数类型选择时要看是否要对类型实例进行修改; 如有修改需求, 则选择*T; 如无修改需求, T 类型 receiver 传值的性能损耗也是考量因素之一。

相关推荐

Vue3非兼容变更——函数式组件(vue 兼容)

在Vue2.X中,函数式组件有两个主要应用场景:作为性能优化,因为它们的初始化速度比有状态组件快得多;返回多个根节点。然而在Vue3.X中,有状态组件的性能已经提高到可以忽略不计的程度。此外,有状态组...

利用vue.js进行组件化开发,一学就会(一)

组件原理/组成组件(Component)扩展HTML元素,封装可重用的代码,核心目标是为了可重用性高,减少重复性的开发。组件预先定义好行为的ViewModel类。代码按照template\styl...

Vue3 新趋势:10 个最强 X 操作!(vue.3)

Vue3为前端开发带来了诸多革新,它不仅提升了性能,还提供了...

总结 Vue3 组件管理 12 种高级写法,灵活使用才能提高效率

SFC单文件组件顾名思义,就是一个.vue文件只写一个组件...

前端流行框架Vue3教程:17. _组件数据传递

_组件数据传递我们之前讲解过了组件之间的数据传递,...

前端流行框架Vue3教程:14. 组件传递Props效验

组件传递Props效验Vue组件可以更细致地声明对传入的props的校验要求...

前端流行框架Vue3教程:25. 组件保持存活

25.组件保持存活当使用...

5 个被低估的 Vue3 实战技巧,让你的项目性能提升 300%?

前端圈最近都在卷性能优化和工程化,你还在用老一套的Vue3开发方法?作为摸爬滚打多年的老前端,今天就把私藏的几个Vue3实战技巧分享出来,帮你在开发效率、代码质量和项目性能上实现弯道超车!一、...

绝望!Vue3 组件频繁崩溃?7 个硬核技巧让性能暴涨 400%!

前端的兄弟姐妹们五一假期快乐,谁还没在Vue3项目上栽过跟头?满心欢喜写好的组件,一到实际场景就频频崩溃,页面加载慢得像蜗牛,操作卡顿到让人想砸电脑。用户疯狂吐槽,领导脸色难看,自己改代码改到怀疑...

前端流行框架Vue3教程:15. 组件事件

组件事件在组件的模板表达式中,可以直接使用...

Vue3,看这篇就够了(vue3 从入门到实战)

一、前言最近很多技术网站,讨论的最多的无非就是Vue3了,大多数都是CompositionAPI和基于Proxy的原理分析。但是今天想着跟大家聊聊,Vue3对于一个低代码平台的前端更深层次意味着什么...

前端流行框架Vue3教程:24.动态组件

24.动态组件有些场景会需要在两个组件间来回切换,比如Tab界面...

前端流行框架Vue3教程:12. 组件的注册方式

组件的注册方式一个Vue组件在使用前需要先被“注册”,这样Vue才能在渲染模板时找到其对应的实现。组件注册有两种方式:全局注册和局部注册...

焦虑!Vue3 组件频繁假死?6 个奇招让页面流畅度狂飙 500%!

前端圈的朋友们,谁还没在Vue3项目上踩过性能的坑?满心期待开发出的组件,一到高并发场景就频繁假死,用户反馈页面点不动,产品经理追着问进度,自己调试到心态炸裂!别以为这是个例,不少人在电商大促、数...

前端流行框架Vue3教程:26. 异步组件

根据上节课的代码,我们在切换到B组件的时候,发现并没有网络请求:异步组件:...

取消回复欢迎 发表评论: